@oh-story/dsh 0.1.8 → 0.1.10
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 +14 -14
- package/lib/client.js +237 -84
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +183 -77
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +140 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +28 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +8 -2
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-edit/SKILL.md +146 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +38 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +29 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +54 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +51 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +520 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +29 -4
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +22 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +13 -12
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +199 -3
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +7 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +10 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +12 -1
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +78 -2
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +12 -5
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +76 -7
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +5 -2
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +21 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -17
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +12 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +61 -5
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -3
- package/lib/drama/skills/short-drama-write/SKILL.md +11 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +290 -90
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +9 -35
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +17 -35
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +16 -34
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +9 -20
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +10 -6
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -32
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +9 -18
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +10 -27
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -6
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +6 -11
- package/lib/oh-story/manifest.json +419 -254
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/consistency-checker.md +4 -4
- package/lib/oh-story/roles/narrative-writer.md +56 -243
- package/lib/oh-story/roles/story-architect.md +6 -6
- package/lib/oh-story/roles/story-explorer.md +11 -11
- package/lib/oh-story/skills/story/SKILL.md +13 -7
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +54 -229
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +78 -82
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-import/references/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +10 -8
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +9 -34
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +197 -15
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +42 -132
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1106 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +4 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +52 -216
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +72 -12
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +6 -15
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +110 -0
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +86 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +9 -34
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +88 -118
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -119
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +73 -54
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +63 -4
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +531 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +197 -15
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +285 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-review/SKILL.md +80 -87
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-review/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +6 -12
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +9 -34
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +58 -43
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +63 -4
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +15 -324
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +55 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +78 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +33 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +52 -0
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -59
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -42
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inspect reusable long-analysis assets without changing user files.
|
|
3
|
+
|
|
4
|
+
Only the upstream directory contract is recognized: ``章节/第N章_摘要.md``,
|
|
5
|
+
golden-three-chapter analyses, and the aggregate files. Missing chapters are
|
|
6
|
+
reported exactly so a run only fills gaps.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import csv
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
import unicodedata
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Iterable
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SUMMARY_RE = re.compile(r"^第0*(\d+)章_摘要\.md$")
|
|
22
|
+
GOLDEN_RE = re.compile(r"^第0*(\d+)章_深度拆解\.md$")
|
|
23
|
+
SCHEMA_RE = re.compile(r"schema_version\s*[::]\s*v?(\d+)", re.IGNORECASE)
|
|
24
|
+
TOTAL_RE = re.compile(r"总章数\s*(?:[::]|\|)\s*(\d+)")
|
|
25
|
+
TOTAL_FALLBACK_RE = re.compile(r"总章数\s+(\d+)\s*章?")
|
|
26
|
+
TABLE_CHAPTER_TOTAL_RE = re.compile(r"\|\s*章节数\s*\|\s*(\d+)\s*(?:章)?\s*\|")
|
|
27
|
+
COVERAGE_TOTAL_RE = re.compile(
|
|
28
|
+
r"(?:输入覆盖\s*[::]\s*全文|精读覆盖\s*[::]\s*第\s*1\s*[—–-]\s*)(\d+)\s*章"
|
|
29
|
+
)
|
|
30
|
+
FINAL_RE = re.compile(r"(?:最终状态|当前状态)\s*[::]\s*([a-z0-9_]+)", re.IGNORECASE)
|
|
31
|
+
SOURCE_HASH_RE = re.compile(
|
|
32
|
+
r"(?:输入版本|SHA-256|source_sha256)\s*(?:[::]|\|)\s*([0-9a-f]{64})", re.IGNORECASE
|
|
33
|
+
)
|
|
34
|
+
PROJECTION_RE = re.compile(
|
|
35
|
+
r"<!--\s*story-long-analyze:projection\s+runtime=(?P<runtime>[^\s]+)\s+"
|
|
36
|
+
r"source=(?P<source>[^\s]+)(?:\s+[^>]*?)?\s*-->"
|
|
37
|
+
)
|
|
38
|
+
STATE_START = "<!-- story-long-analyze:runtime-state:start -->"
|
|
39
|
+
STATE_END = "<!-- story-long-analyze:runtime-state:end -->"
|
|
40
|
+
# v0.7.x wrote a 「章节边界」 table (章号 | 标题 | 起始行 | 字数) into _progress.md.
|
|
41
|
+
BOUNDARY_HEADING_RE = re.compile(r"^#{1,6}\s*章节边界")
|
|
42
|
+
TITLE_LABEL_RE = re.compile(
|
|
43
|
+
r"^\s*(?:第(?P<number>[〇零一二三四五六七八九十百千万两0-9]+)章|第[〇零一二三四五六七八九十百千万两0-9]+[卷回节]"
|
|
44
|
+
r"|卷[〇零一二三四五六七八九十百千万两0-9]+|Chapter\s*(?P<english>[0-9]+)|(?P<numeric>[0-9]+)[.、](?![0-9])"
|
|
45
|
+
r"|(?P<special>楔子|序章|引子|前言|后记|尾声|番外[〇零一二三四五六七八九十百千万两0-9]*))",
|
|
46
|
+
re.IGNORECASE,
|
|
47
|
+
)
|
|
48
|
+
# Author notes appended to a heading, e.g. 「(求收藏)」「【二合一】」.
|
|
49
|
+
TITLE_NOTE_RE = re.compile(r"[(\[【〔〖][^()\[\]【】〔〕〖〗]*[)\]】〕〗]\s*$")
|
|
50
|
+
TITLE_NOISE_RE = re.compile(r"[\s\-—::、.,;!?\"“”'‘’《》「」『』()\[\]【】〔〕〖〗]+")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def nonempty(path: Path) -> bool:
|
|
54
|
+
try:
|
|
55
|
+
return path.is_file() and path.stat().st_size > 0
|
|
56
|
+
except OSError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def any_nonempty(paths: Iterable[Path]) -> bool:
|
|
61
|
+
return any(nonempty(path) for path in paths)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_text(path: Path) -> str | None:
|
|
65
|
+
if not nonempty(path):
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
return path.read_text(encoding="utf-8-sig")
|
|
69
|
+
except (OSError, UnicodeError):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_progress(path: Path) -> tuple[int | None, int | None, str | None]:
|
|
74
|
+
text = read_text(path)
|
|
75
|
+
if text is None:
|
|
76
|
+
return None, None, "unreadable" if path.exists() else None
|
|
77
|
+
schema_match = SCHEMA_RE.search(text)
|
|
78
|
+
total_match = TOTAL_RE.search(text) or COVERAGE_TOTAL_RE.search(text)
|
|
79
|
+
final_match = FINAL_RE.search(text)
|
|
80
|
+
return (
|
|
81
|
+
int(schema_match.group(1)) if schema_match else None,
|
|
82
|
+
int(total_match.group(1)) if total_match else None,
|
|
83
|
+
final_match.group(1).lower() if final_match else None,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def read_declared_total(paths: Iterable[Path]) -> tuple[int | None, str | None]:
|
|
88
|
+
for path in paths:
|
|
89
|
+
text = read_text(path)
|
|
90
|
+
if text is None:
|
|
91
|
+
continue
|
|
92
|
+
match = TOTAL_RE.search(text) or TOTAL_FALLBACK_RE.search(text) or TABLE_CHAPTER_TOTAL_RE.search(text)
|
|
93
|
+
if match and int(match.group(1)) > 0:
|
|
94
|
+
return int(match.group(1)), path.as_posix()
|
|
95
|
+
return None, None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def read_source_hash(paths: Iterable[Path]) -> tuple[str | None, str | None]:
|
|
99
|
+
for path in paths:
|
|
100
|
+
text = read_text(path)
|
|
101
|
+
if text is None:
|
|
102
|
+
continue
|
|
103
|
+
match = SOURCE_HASH_RE.search(text)
|
|
104
|
+
if match:
|
|
105
|
+
return match.group(1).lower(), path.as_posix()
|
|
106
|
+
return None, None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_index_chapters(path: Path) -> tuple[list[int], list[str]]:
|
|
110
|
+
if not nonempty(path):
|
|
111
|
+
return [], []
|
|
112
|
+
errors: list[str] = []
|
|
113
|
+
chapters: list[int] = []
|
|
114
|
+
try:
|
|
115
|
+
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
116
|
+
reader = csv.DictReader(handle)
|
|
117
|
+
required = {"chapter", "char_count", "source_locator", "status", "source_sha256"}
|
|
118
|
+
missing = required - set(reader.fieldnames or [])
|
|
119
|
+
if missing:
|
|
120
|
+
return [], ["chapter_index.csv 缺列:" + ", ".join(sorted(missing))]
|
|
121
|
+
for row_number, row in enumerate(reader, start=2):
|
|
122
|
+
try:
|
|
123
|
+
chapters.append(int(row["chapter"]))
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
errors.append(f"chapter_index.csv 第 {row_number} 行章号无效")
|
|
126
|
+
except (OSError, UnicodeError, csv.Error) as exc:
|
|
127
|
+
return [], [f"chapter_index.csv 不可读:{exc}"]
|
|
128
|
+
if len(chapters) != len(set(chapters)):
|
|
129
|
+
errors.append("chapter_index.csv 存在重复章号")
|
|
130
|
+
unique = sorted(set(chapters))
|
|
131
|
+
if unique and unique != list(range(1, max(unique) + 1)):
|
|
132
|
+
errors.append("chapter_index.csv 章号不连续")
|
|
133
|
+
return unique, errors
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def read_index_identities(path: Path) -> list[dict[str, str]]:
|
|
137
|
+
if not nonempty(path):
|
|
138
|
+
return []
|
|
139
|
+
try:
|
|
140
|
+
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
|
141
|
+
return [dict(row) for row in csv.DictReader(handle)]
|
|
142
|
+
except (OSError, UnicodeError, csv.Error):
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read_managed_stages(text: str | None) -> tuple[bool, dict[str, dict[str, str]]]:
|
|
147
|
+
if not text or text.count(STATE_START) != 1 or text.count(STATE_END) != 1:
|
|
148
|
+
return False, {}
|
|
149
|
+
match = re.search(re.escape(STATE_START) + r"(.*?)" + re.escape(STATE_END), text, re.DOTALL)
|
|
150
|
+
if not match:
|
|
151
|
+
return False, {}
|
|
152
|
+
stages: dict[str, dict[str, str]] = {}
|
|
153
|
+
section = None
|
|
154
|
+
for line in match.group(1).splitlines():
|
|
155
|
+
if line.strip() == "### 阶段状态":
|
|
156
|
+
section = "stages"
|
|
157
|
+
continue
|
|
158
|
+
if line.startswith("### "):
|
|
159
|
+
section = None
|
|
160
|
+
continue
|
|
161
|
+
if section != "stages" or not line.lstrip().startswith("|"):
|
|
162
|
+
continue
|
|
163
|
+
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
164
|
+
if len(cells) >= 3 and cells[0] not in {"阶段", "---"}:
|
|
165
|
+
stages[cells[0].lower()] = {"status": cells[1].lower(), "output": cells[2]}
|
|
166
|
+
return True, stages
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def collect_numbered_files(
|
|
170
|
+
directory: Path, pattern: re.Pattern[str]
|
|
171
|
+
) -> tuple[list[int], dict[int, Path], list[str]]:
|
|
172
|
+
chapters: list[int] = []
|
|
173
|
+
sources: dict[int, Path] = {}
|
|
174
|
+
duplicates: list[str] = []
|
|
175
|
+
if not directory.is_dir():
|
|
176
|
+
return chapters, sources, duplicates
|
|
177
|
+
for path in sorted(directory.iterdir()):
|
|
178
|
+
match = pattern.match(path.name)
|
|
179
|
+
if not match or not nonempty(path):
|
|
180
|
+
continue
|
|
181
|
+
chapter = int(match.group(1))
|
|
182
|
+
if chapter in sources:
|
|
183
|
+
duplicates.append(f"第{chapter}章:{sources[chapter].as_posix()} / {path.as_posix()}")
|
|
184
|
+
continue
|
|
185
|
+
chapters.append(chapter)
|
|
186
|
+
sources[chapter] = path
|
|
187
|
+
return sorted(chapters), sources, duplicates
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def compact_ranges(chapters: Iterable[int]) -> list[str]:
|
|
191
|
+
values = sorted(set(chapters))
|
|
192
|
+
if not values:
|
|
193
|
+
return []
|
|
194
|
+
ranges: list[str] = []
|
|
195
|
+
start = previous = values[0]
|
|
196
|
+
for chapter in values[1:]:
|
|
197
|
+
if chapter == previous + 1:
|
|
198
|
+
previous = chapter
|
|
199
|
+
continue
|
|
200
|
+
ranges.append(str(start) if start == previous else f"{start}-{previous}")
|
|
201
|
+
start = previous = chapter
|
|
202
|
+
ranges.append(str(start) if start == previous else f"{start}-{previous}")
|
|
203
|
+
return ranges
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def relative(root: Path, path: Path) -> str:
|
|
207
|
+
try:
|
|
208
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
209
|
+
except (OSError, ValueError):
|
|
210
|
+
return path.as_posix()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def summary_kind(path: Path) -> str:
|
|
214
|
+
text = read_text(path) or ""
|
|
215
|
+
return "three_script_projection" if PROJECTION_RE.search(text[:1000]) else "upstream_summary"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def read_legacy_boundaries(text: str | None) -> list[dict[str, object]]:
|
|
219
|
+
"""Rows of the v0.7.x 「章节边界」 table: old chapter number, title, start line."""
|
|
220
|
+
if not text:
|
|
221
|
+
return []
|
|
222
|
+
rows: list[dict[str, object]] = []
|
|
223
|
+
in_section = False
|
|
224
|
+
header: list[str] | None = None
|
|
225
|
+
for line in text.splitlines():
|
|
226
|
+
stripped = line.strip()
|
|
227
|
+
if stripped.startswith("#"):
|
|
228
|
+
if in_section and rows:
|
|
229
|
+
break
|
|
230
|
+
in_section = bool(BOUNDARY_HEADING_RE.match(stripped))
|
|
231
|
+
header = None
|
|
232
|
+
continue
|
|
233
|
+
if not in_section or not stripped.startswith("|"):
|
|
234
|
+
continue
|
|
235
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
236
|
+
if header is None:
|
|
237
|
+
if "章号" in cells:
|
|
238
|
+
header = cells
|
|
239
|
+
continue
|
|
240
|
+
if not "".join(cells).strip("-: "):
|
|
241
|
+
continue
|
|
242
|
+
record = dict(zip(header, cells))
|
|
243
|
+
number = re.search(r"\d+", record.get("章号", ""))
|
|
244
|
+
start = re.search(r"\d+", record.get("起始行", ""))
|
|
245
|
+
if not number:
|
|
246
|
+
continue
|
|
247
|
+
rows.append({
|
|
248
|
+
"chapter": int(number.group(0)),
|
|
249
|
+
"title": record.get("标题", ""),
|
|
250
|
+
"start_line": int(start.group(0)) if start else None,
|
|
251
|
+
})
|
|
252
|
+
return rows
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def title_parts(title: str) -> tuple[str, str, str]:
|
|
256
|
+
"""(label, strict key, loose key) of a heading; loose drops trailing author notes."""
|
|
257
|
+
text = unicodedata.normalize("NFKC", str(title or "")).strip()
|
|
258
|
+
label = ""
|
|
259
|
+
for _ in range(3):
|
|
260
|
+
match = TITLE_LABEL_RE.match(text)
|
|
261
|
+
if not match or not match.group(0).strip():
|
|
262
|
+
break
|
|
263
|
+
for group in ("number", "english", "numeric", "special"):
|
|
264
|
+
if match.group(group):
|
|
265
|
+
label = match.group(group)
|
|
266
|
+
text = text[match.end():]
|
|
267
|
+
strict = TITLE_NOISE_RE.sub("", text)
|
|
268
|
+
loose = text
|
|
269
|
+
while TITLE_NOTE_RE.search(loose):
|
|
270
|
+
loose = TITLE_NOTE_RE.sub("", loose)
|
|
271
|
+
return label, strict, TITLE_NOISE_RE.sub("", loose) or strict
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def label_matches(label: str, row: dict[str, object]) -> bool:
|
|
275
|
+
source = str(row.get("source_chapter", "")).strip()
|
|
276
|
+
if not label or not source:
|
|
277
|
+
return False
|
|
278
|
+
if label == source:
|
|
279
|
+
return True
|
|
280
|
+
try:
|
|
281
|
+
from build_chapter_index import parse_number
|
|
282
|
+
return source.isdigit() and parse_number(label) == int(source)
|
|
283
|
+
except ValueError:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def title_candidates(title: str, index_rows: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
288
|
+
"""Index rows whose title agrees with an old table title, best tier first."""
|
|
289
|
+
label, strict, loose = title_parts(title)
|
|
290
|
+
if strict:
|
|
291
|
+
keys = [(row, title_parts(str(row.get("title", "")))) for row in index_rows]
|
|
292
|
+
for tier in (
|
|
293
|
+
[row for row, (_, row_strict, _) in keys if row_strict == strict],
|
|
294
|
+
[row for row, (_, _, row_loose) in keys
|
|
295
|
+
if row_loose and (row_loose == loose or row_loose in loose or loose in row_loose)],
|
|
296
|
+
):
|
|
297
|
+
if tier:
|
|
298
|
+
return tier
|
|
299
|
+
# Retitled or title-less headings still carry their chapter number.
|
|
300
|
+
return [row for row in index_rows if label_matches(label, row)]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def map_legacy_row(old: dict[str, object], index_rows: list[dict[str, object]]) -> int | None:
|
|
304
|
+
"""Index chapter an old boundary row points at.
|
|
305
|
+
|
|
306
|
+
A start line that is exactly a chapter's heading line decides on its own.
|
|
307
|
+
Otherwise the title picks the chapter (nearest to the old start line when
|
|
308
|
+
several match); a title-less row falls back to the chapter holding the line.
|
|
309
|
+
"""
|
|
310
|
+
start = old.get("start_line")
|
|
311
|
+
if isinstance(start, int):
|
|
312
|
+
for row in index_rows:
|
|
313
|
+
if int(row["start_line"]) == start:
|
|
314
|
+
return int(row["chapter"])
|
|
315
|
+
matches = title_candidates(str(old.get("title", "")), index_rows)
|
|
316
|
+
if not isinstance(start, int):
|
|
317
|
+
return int(matches[0]["chapter"]) if len(matches) == 1 else None
|
|
318
|
+
if not matches and not any(title_parts(str(old.get("title", "")))[:2]):
|
|
319
|
+
matches = [row for row in index_rows if int(row["start_line"]) <= start <= int(row["end_line"])]
|
|
320
|
+
|
|
321
|
+
def distance(row: dict[str, object]) -> int:
|
|
322
|
+
first, last = int(row["start_line"]), int(row["end_line"])
|
|
323
|
+
return 0 if first <= start <= last else min(abs(start - first), abs(start - last))
|
|
324
|
+
|
|
325
|
+
ranked = sorted(matches, key=distance)
|
|
326
|
+
if not ranked or (len(ranked) > 1 and distance(ranked[0]) == distance(ranked[1])):
|
|
327
|
+
return None
|
|
328
|
+
return int(ranked[0]["chapter"])
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def leading_special_labels(index_rows: list[dict[str, object]]) -> list[str]:
|
|
332
|
+
"""Prologue-like chapters (楔子、序章、第0章…) before the first numbered chapter."""
|
|
333
|
+
labels: list[str] = []
|
|
334
|
+
for row in index_rows:
|
|
335
|
+
source = str(row.get("source_chapter", "")).strip()
|
|
336
|
+
if source.isdigit() and int(source) >= 1:
|
|
337
|
+
break
|
|
338
|
+
labels.append("第0章" if source == "0" else source)
|
|
339
|
+
return labels
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def mapping_author_message(detail: str, labels: list[str], done: str) -> str:
|
|
343
|
+
"""One plain-language stop message with the author's choices (no field names)."""
|
|
344
|
+
if labels:
|
|
345
|
+
prologue = "、".join("「%s」" % label for label in labels)
|
|
346
|
+
return (
|
|
347
|
+
"先停一下:%s。这次重新识别章节时,开头的%s被算成了单独一章,"
|
|
348
|
+
"照这样续拆,旧文件会整体错开一章——%s没人拆、有的章被拆两遍。请选一种:"
|
|
349
|
+
"① 按旧章号继续(推荐,旧拆文当时没把%s算作一章时选这个):%s并进第一章,"
|
|
350
|
+
"已拆好的%s原样复用,%s不单独拆;"
|
|
351
|
+
"② %s单独算一章:这本书已有的拆文结果全部挪进备份目录(不删除),按新章号重拆;"
|
|
352
|
+
"③ 换一个新目录,整本重新拆。"
|
|
353
|
+
% (detail, prologue, prologue, prologue, prologue, done, prologue, prologue)
|
|
354
|
+
)
|
|
355
|
+
return (
|
|
356
|
+
"先停一下:%s,没法确认已拆好的%s各对应哪一章。请选一种:"
|
|
357
|
+
"① 换一个新目录,整本重新拆(推荐);② 如果原文换过版本或被改过,换回拆文时用的那份原文再续拆。"
|
|
358
|
+
% (detail, done)
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def legacy_mapping_check(root: Path, index_rows: list[dict[str, object]]) -> dict[str, object] | None:
|
|
363
|
+
"""Stop when files numbered by an older run no longer match the chapter index.
|
|
364
|
+
|
|
365
|
+
Old summaries always predate the index. Golden-chapter analyses count as old
|
|
366
|
+
when ``_progress.md`` comes from v0.7.x (a 「章节边界」 table or no runtime
|
|
367
|
+
block); this run's Stage 1 writes them against the index. The old boundary
|
|
368
|
+
table, when present, decides; otherwise a source that does not start at
|
|
369
|
+
chapter one is ambiguous.
|
|
370
|
+
"""
|
|
371
|
+
if not index_rows:
|
|
372
|
+
return None
|
|
373
|
+
progress_text = read_text(root / "_progress.md")
|
|
374
|
+
boundaries = read_legacy_boundaries(progress_text)
|
|
375
|
+
legacy_progress = progress_text is not None and (bool(boundaries) or STATE_START not in progress_text)
|
|
376
|
+
_, summary_sources, _ = collect_numbered_files(root / "章节", SUMMARY_RE)
|
|
377
|
+
old_chapters = {chapter for chapter, path in summary_sources.items() if summary_kind(path) == "upstream_summary"}
|
|
378
|
+
old_kinds = ["逐章摘要"] if old_chapters else []
|
|
379
|
+
if legacy_progress:
|
|
380
|
+
golden, _, _ = collect_numbered_files(root / "章节", GOLDEN_RE)
|
|
381
|
+
if golden:
|
|
382
|
+
old_chapters.update(golden)
|
|
383
|
+
old_kinds.insert(0, "开头三章拆解")
|
|
384
|
+
if not old_chapters:
|
|
385
|
+
return None
|
|
386
|
+
rows = sorted(index_rows, key=lambda row: int(row["chapter"]))
|
|
387
|
+
labels = leading_special_labels(rows)
|
|
388
|
+
done = "、".join(old_kinds)
|
|
389
|
+
if boundaries:
|
|
390
|
+
by_old = {int(row["chapter"]): row for row in boundaries}
|
|
391
|
+
# Chapters the old run appended after its table stay on the same numbering
|
|
392
|
+
# as long as every chapter the table does cover lines up.
|
|
393
|
+
checked = sorted(chapter for chapter in old_chapters if chapter <= max(by_old)) or sorted(old_chapters)
|
|
394
|
+
shifted = []
|
|
395
|
+
unknown = []
|
|
396
|
+
for chapter in checked:
|
|
397
|
+
old = by_old.get(chapter)
|
|
398
|
+
mapped = map_legacy_row(old, rows) if old else None
|
|
399
|
+
if mapped is None:
|
|
400
|
+
unknown.append(chapter)
|
|
401
|
+
elif mapped != chapter:
|
|
402
|
+
shifted.append((chapter, mapped))
|
|
403
|
+
if not shifted and not unknown:
|
|
404
|
+
return None
|
|
405
|
+
if shifted:
|
|
406
|
+
old_number, new_number = shifted[0]
|
|
407
|
+
title = next((str(row.get("title", "")) for row in rows if int(row["chapter"]) == new_number), "")
|
|
408
|
+
detail = "旧拆文的第%s章「%s」,在这次的章节表里是第%s章" % (old_number, title, new_number)
|
|
409
|
+
else:
|
|
410
|
+
detail = "旧进度里的章节表和原文对不上(第%s章找不到)" % compact_ranges(unknown)[0]
|
|
411
|
+
code = "legacy_boundary_shift" if shifted else "legacy_boundary_unmatched"
|
|
412
|
+
else:
|
|
413
|
+
first_source = str(rows[0].get("source_chapter", "")).strip()
|
|
414
|
+
if first_source.isdigit() and int(first_source) == 1:
|
|
415
|
+
return None
|
|
416
|
+
detail = "原文开头是「%s」,而旧拆文没有留下能核对章号的章节表" % (first_source or "未知")
|
|
417
|
+
code = "legacy_identity_unverifiable"
|
|
418
|
+
return {
|
|
419
|
+
"code": code,
|
|
420
|
+
"conflict": "旧拆文章号与当前索引不一致:" + detail,
|
|
421
|
+
"author_message": mapping_author_message(detail, labels, done),
|
|
422
|
+
"fold_prologue_available": bool(labels),
|
|
423
|
+
"legacy_chapters": sorted(old_chapters),
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def inspect(root: Path, expected_override: int | None) -> dict[str, object]:
|
|
428
|
+
root = root.resolve()
|
|
429
|
+
schema, progress_total, final_state = read_progress(root / "_progress.md")
|
|
430
|
+
progress_text = read_text(root / "_progress.md")
|
|
431
|
+
index_chapters, index_errors = read_index_chapters(root / "chapter_index.csv")
|
|
432
|
+
index_identities = read_index_identities(root / "chapter_index.csv")
|
|
433
|
+
managed_state_present, managed_stages = read_managed_stages(progress_text)
|
|
434
|
+
|
|
435
|
+
summaries, summary_sources, duplicate_summaries = collect_numbered_files(root / "章节", SUMMARY_RE)
|
|
436
|
+
golden, golden_sources, duplicate_golden = collect_numbered_files(root / "章节", GOLDEN_RE)
|
|
437
|
+
|
|
438
|
+
preferred_paths: dict[int, str] = {}
|
|
439
|
+
preferred_kinds: dict[int, str] = {}
|
|
440
|
+
for chapter, path in summary_sources.items():
|
|
441
|
+
preferred_paths[chapter] = relative(root, path)
|
|
442
|
+
preferred_kinds[chapter] = summary_kind(path)
|
|
443
|
+
for chapter, path in golden_sources.items():
|
|
444
|
+
if chapter not in preferred_paths:
|
|
445
|
+
preferred_paths[chapter] = relative(root, path)
|
|
446
|
+
preferred_kinds[chapter] = "golden_analysis"
|
|
447
|
+
|
|
448
|
+
expected = expected_override
|
|
449
|
+
expected_source = "argument" if expected is not None else None
|
|
450
|
+
if expected is None and index_chapters and not index_errors:
|
|
451
|
+
expected = max(index_chapters)
|
|
452
|
+
expected_source = "chapter_index.csv"
|
|
453
|
+
if expected is None and progress_total is not None and final_state in {"completed", "completed_with_errors"}:
|
|
454
|
+
expected = progress_total
|
|
455
|
+
expected_source = "_progress.md"
|
|
456
|
+
if expected is None:
|
|
457
|
+
expected, source = read_declared_total((root / "拆文报告.md", root / "概要.md", root / "快速预览.md"))
|
|
458
|
+
expected_source = Path(source).name if source else None
|
|
459
|
+
if expected is None and progress_total is not None:
|
|
460
|
+
expected = progress_total
|
|
461
|
+
expected_source = "_progress.md"
|
|
462
|
+
|
|
463
|
+
expected_set = set(range(1, expected + 1)) if expected else set()
|
|
464
|
+
union_semantic_set = set(preferred_paths)
|
|
465
|
+
missing_semantic = sorted(expected_set - union_semantic_set)
|
|
466
|
+
out_of_range_semantic = sorted(union_semantic_set - expected_set) if expected else []
|
|
467
|
+
missing_summaries = sorted(expected_set - set(summaries))
|
|
468
|
+
out_of_range_summaries = sorted(set(summaries) - expected_set) if expected else []
|
|
469
|
+
|
|
470
|
+
primary = {
|
|
471
|
+
"emotion_module": nonempty(root / "剧情" / "情绪模块.md"),
|
|
472
|
+
"rhythm": nonempty(root / "剧情" / "节奏.md"),
|
|
473
|
+
}
|
|
474
|
+
plot_dir = root / "剧情"
|
|
475
|
+
assets = {
|
|
476
|
+
"report": nonempty(root / "拆文报告.md"),
|
|
477
|
+
"style": nonempty(root / "文风.md"),
|
|
478
|
+
"storyline": nonempty(plot_dir / "故事线.md"),
|
|
479
|
+
"plot_units": any_nonempty(
|
|
480
|
+
path
|
|
481
|
+
for path in plot_dir.glob("*.md")
|
|
482
|
+
if path.name not in {"README.md", "故事线.md", "节奏.md", "情绪模块.md", "散落情节.md"}
|
|
483
|
+
)
|
|
484
|
+
if plot_dir.is_dir()
|
|
485
|
+
else False,
|
|
486
|
+
"characters": any_nonempty((root / "角色").glob("*.md")) if (root / "角色").is_dir() else False,
|
|
487
|
+
"settings": any_nonempty((root / "设定").rglob("*.md")) if (root / "设定").is_dir() else False,
|
|
488
|
+
"batch_cache": any_nonempty((root / "_analysis_cache").glob("批次-*.md"))
|
|
489
|
+
if (root / "_analysis_cache").is_dir()
|
|
490
|
+
else False,
|
|
491
|
+
"chapter_index": nonempty(root / "chapter_index.csv"),
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
has_any = bool(union_semantic_set) or any(assets.values()) or any(primary.values()) or nonempty(root / "_progress.md")
|
|
495
|
+
semantic_coverage_complete = bool(union_semantic_set) and (
|
|
496
|
+
(expected is not None and not missing_semantic and not out_of_range_semantic)
|
|
497
|
+
or (expected is None and final_state in {"completed", "completed_with_errors"})
|
|
498
|
+
)
|
|
499
|
+
summary_coverage_complete = (
|
|
500
|
+
bool(summaries) and expected is not None and not missing_summaries and not out_of_range_summaries
|
|
501
|
+
)
|
|
502
|
+
standard_coverage_complete = bool(union_semantic_set) and expected is not None and union_semantic_set == expected_set
|
|
503
|
+
legacy_core = assets["report"] and summary_coverage_complete and (assets["storyline"] or assets["plot_units"])
|
|
504
|
+
managed_pipeline_complete = all(
|
|
505
|
+
managed_stages.get(stage, {}).get("status") == "completed"
|
|
506
|
+
for stage in ("stage1", "stage2", "stage3", "stage4", "stage5", "stage6")
|
|
507
|
+
)
|
|
508
|
+
usable_upstream_complete = (
|
|
509
|
+
all(primary.values())
|
|
510
|
+
and assets["report"]
|
|
511
|
+
and standard_coverage_complete
|
|
512
|
+
and (
|
|
513
|
+
managed_pipeline_complete
|
|
514
|
+
or (
|
|
515
|
+
final_state in {"completed", "completed_with_errors"}
|
|
516
|
+
and (assets["storyline"] or assets["plot_units"])
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if expected:
|
|
522
|
+
preferred_paths = {chapter: path for chapter, path in preferred_paths.items() if chapter in expected_set}
|
|
523
|
+
preferred_kinds = {chapter: kind for chapter, kind in preferred_kinds.items() if chapter in expected_set}
|
|
524
|
+
semantic_set = set(preferred_paths)
|
|
525
|
+
|
|
526
|
+
full_result_available = usable_upstream_complete or legacy_core
|
|
527
|
+
mixed_sources = len(set(preferred_kinds.values())) > 1
|
|
528
|
+
|
|
529
|
+
# Files numbered by an older run can drift from the index; this run's golden
|
|
530
|
+
# analyses and projections are written against it.
|
|
531
|
+
mapping_conflicts: list[str] = []
|
|
532
|
+
mapping_blocked_chapters: list[int] = []
|
|
533
|
+
mapping = legacy_mapping_check(root, index_identities) if index_identities else None
|
|
534
|
+
if mapping:
|
|
535
|
+
mapping_blocked_chapters = sorted(expected_set or set(index_chapters))
|
|
536
|
+
mapping_conflicts.append(str(mapping["conflict"]))
|
|
537
|
+
|
|
538
|
+
stage_repairs: list[str] = []
|
|
539
|
+
if semantic_coverage_complete:
|
|
540
|
+
legacy_completed = final_state in {"completed", "completed_with_errors"}
|
|
541
|
+
if not primary["emotion_module"]:
|
|
542
|
+
stage_repairs.append("stage3_emotion")
|
|
543
|
+
if not primary["rhythm"]:
|
|
544
|
+
stage_repairs.append("stage3_rhythm")
|
|
545
|
+
if not legacy_completed and (not assets["characters"] or not assets["settings"]):
|
|
546
|
+
stage_repairs.append("stage4")
|
|
547
|
+
if not assets["report"]:
|
|
548
|
+
stage_repairs.append("stage5")
|
|
549
|
+
if not assets["style"]:
|
|
550
|
+
stage_repairs.append("stage6_style")
|
|
551
|
+
for stage in ("stage3", "stage4", "stage5", "stage6"):
|
|
552
|
+
row = managed_stages.get(stage)
|
|
553
|
+
if row and row.get("status") != "completed" and stage not in stage_repairs:
|
|
554
|
+
stage_repairs.append(stage)
|
|
555
|
+
elif managed_state_present and not legacy_completed and row is None:
|
|
556
|
+
# New three-script runs require a completed stage row even when
|
|
557
|
+
# a file was manually placed before the interruption.
|
|
558
|
+
if stage not in stage_repairs and not any(item.startswith(stage + "_") for item in stage_repairs):
|
|
559
|
+
stage_repairs.append(stage)
|
|
560
|
+
|
|
561
|
+
if not has_any:
|
|
562
|
+
classification = "empty"
|
|
563
|
+
recommended_path = "new_analysis"
|
|
564
|
+
elif usable_upstream_complete:
|
|
565
|
+
classification = "current_complete"
|
|
566
|
+
recommended_path = "repair_stages" if stage_repairs else "direct_use"
|
|
567
|
+
elif legacy_core:
|
|
568
|
+
classification = "legacy_complete"
|
|
569
|
+
recommended_path = "repair_stages" if stage_repairs else "direct_use"
|
|
570
|
+
elif semantic_coverage_complete:
|
|
571
|
+
classification = "partial_or_mixed"
|
|
572
|
+
recommended_path = "repair_stages" if stage_repairs else "enhance_existing"
|
|
573
|
+
else:
|
|
574
|
+
classification = "partial_or_mixed"
|
|
575
|
+
recommended_path = "continue_partial" if expected and missing_semantic else "enhance_existing"
|
|
576
|
+
|
|
577
|
+
conflicts = list(index_errors)
|
|
578
|
+
conflicts.extend(f"重复摘要:{item}" for item in duplicate_summaries)
|
|
579
|
+
conflicts.extend(f"重复黄金三章:{item}" for item in duplicate_golden)
|
|
580
|
+
conflicts.extend(mapping_conflicts)
|
|
581
|
+
if final_state == "completed" and not usable_upstream_complete:
|
|
582
|
+
conflicts.append("进度标记 completed,但当前主产物或上游逐章覆盖不完整")
|
|
583
|
+
if final_state == "completed_with_errors" and not semantic_coverage_complete:
|
|
584
|
+
conflicts.append("进度标记 completed_with_errors,需按失败与待核记录确认可用范围")
|
|
585
|
+
if index_chapters and expected and max(index_chapters) != expected:
|
|
586
|
+
conflicts.append("机械索引章数与期望章数不一致")
|
|
587
|
+
|
|
588
|
+
source_hash, source_hash_path = read_source_hash((root / "_progress.md",))
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
"root": str(root),
|
|
592
|
+
"classification": classification,
|
|
593
|
+
"recommended_path": recommended_path,
|
|
594
|
+
"schema_version": schema,
|
|
595
|
+
"final_state": final_state,
|
|
596
|
+
"expected_chapters": expected,
|
|
597
|
+
"expected_chapters_source": expected_source,
|
|
598
|
+
# Original keys remain for callers that need the upstream interface itself.
|
|
599
|
+
"completed_summary_chapters": summaries,
|
|
600
|
+
"missing_summary_chapters": missing_summaries,
|
|
601
|
+
"out_of_range_summary_chapters": out_of_range_summaries,
|
|
602
|
+
# Runtime routing must use the selected semantic family below, never summary filenames alone.
|
|
603
|
+
"completed_semantic_chapters": sorted(semantic_set),
|
|
604
|
+
"missing_semantic_chapters": missing_semantic,
|
|
605
|
+
"out_of_range_semantic_chapters": out_of_range_semantic,
|
|
606
|
+
"semantic_coverage_complete": semantic_coverage_complete,
|
|
607
|
+
"full_result_available": full_result_available,
|
|
608
|
+
"chapter_sources": {
|
|
609
|
+
"priority": ["upstream_summary", "three_script_projection", "golden_analysis"],
|
|
610
|
+
"upstream_summary": {"chapters": summaries, "ranges": compact_ranges(summaries)},
|
|
611
|
+
"golden_analysis": {"chapters": golden, "ranges": compact_ranges(golden)},
|
|
612
|
+
"preferred_by_chapter": {str(chapter): preferred_kinds[chapter] for chapter in sorted(preferred_kinds)},
|
|
613
|
+
"preferred_paths": {str(chapter): preferred_paths[chapter] for chapter in sorted(preferred_paths)},
|
|
614
|
+
},
|
|
615
|
+
"source_hash": source_hash,
|
|
616
|
+
"source_hash_source": source_hash_path,
|
|
617
|
+
"primary_artifacts": primary,
|
|
618
|
+
"reusable_assets": assets,
|
|
619
|
+
"legacy_capabilities": {
|
|
620
|
+
"benchmark_reference": full_result_available or assets["report"] or assets["plot_units"] or bool(semantic_set),
|
|
621
|
+
"import_facts": bool(semantic_set),
|
|
622
|
+
"writing_style_reference": assets["style"],
|
|
623
|
+
},
|
|
624
|
+
"current_capabilities": {
|
|
625
|
+
"emotion_module_recall": primary["emotion_module"],
|
|
626
|
+
"rhythm_reference_recall": primary["rhythm"],
|
|
627
|
+
"source_location": assets["chapter_index"],
|
|
628
|
+
},
|
|
629
|
+
"mixed_sources": mixed_sources,
|
|
630
|
+
"stage_repairs": stage_repairs,
|
|
631
|
+
"managed_stage_status": managed_stages,
|
|
632
|
+
"chapter_mapping_conflicts": mapping_conflicts,
|
|
633
|
+
"chapter_mapping_blocked_chapters": mapping_blocked_chapters,
|
|
634
|
+
"chapter_mapping_author_message": mapping["author_message"] if mapping else None,
|
|
635
|
+
"provenance_requires_review": mixed_sources or (has_any and schema is None),
|
|
636
|
+
"conflicts": conflicts,
|
|
637
|
+
"notes": [
|
|
638
|
+
"本检查只扫描传入书目目录中的上游标准路径,不扫描其他项目或磁盘。",
|
|
639
|
+
"运行路由必须使用 completed_semantic_chapters;缺少逐章摘要文件不等于缺少语义成果。",
|
|
640
|
+
"direct_use 表示默认直接复用;只有用户明确要求增强时才二次提取已有成果。",
|
|
641
|
+
"整本重拆换一个新目录;本检查不提供就地重拆入口。",
|
|
642
|
+
],
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def compact_payload(payload: dict[str, object]) -> dict[str, object]:
|
|
647
|
+
"""Remove per-chapter arrays from the human/model-facing inspection view."""
|
|
648
|
+
result = dict(payload)
|
|
649
|
+
semantic = result.pop("completed_semantic_chapters")
|
|
650
|
+
missing = result.pop("missing_semantic_chapters")
|
|
651
|
+
result["semantic_coverage"] = {
|
|
652
|
+
"completed_count": len(semantic),
|
|
653
|
+
"completed_ranges": compact_ranges(semantic),
|
|
654
|
+
"missing_count": len(missing),
|
|
655
|
+
"missing_ranges": compact_ranges(missing),
|
|
656
|
+
}
|
|
657
|
+
result.pop("completed_summary_chapters", None)
|
|
658
|
+
result.pop("missing_summary_chapters", None)
|
|
659
|
+
result.pop("out_of_range_summary_chapters", None)
|
|
660
|
+
result.pop("out_of_range_semantic_chapters", None)
|
|
661
|
+
source_info = dict(result["chapter_sources"])
|
|
662
|
+
source_info.pop("preferred_by_chapter", None)
|
|
663
|
+
source_info.pop("preferred_paths", None)
|
|
664
|
+
for key in ("upstream_summary", "golden_analysis"):
|
|
665
|
+
entry = dict(source_info[key])
|
|
666
|
+
entry["count"] = len(entry.pop("chapters"))
|
|
667
|
+
source_info[key] = entry
|
|
668
|
+
result["chapter_sources"] = source_info
|
|
669
|
+
return result
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def main() -> int:
|
|
673
|
+
for stream in (sys.stdout, sys.stderr):
|
|
674
|
+
if hasattr(stream, "reconfigure"):
|
|
675
|
+
stream.reconfigure(encoding="utf-8")
|
|
676
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
677
|
+
parser.add_argument("--root", type=Path, required=True, help="拆文库/{书名} 目录")
|
|
678
|
+
parser.add_argument("--expected-chapters", type=int, help="已知总章数;省略时按标准路径优先推断")
|
|
679
|
+
parser.add_argument("--compact", action="store_true", help="省略逐章数组,输出适合运行路由读取的范围摘要")
|
|
680
|
+
args = parser.parse_args()
|
|
681
|
+
if args.expected_chapters is not None and args.expected_chapters < 1:
|
|
682
|
+
parser.error("--expected-chapters 必须大于 0")
|
|
683
|
+
try:
|
|
684
|
+
if not args.root.exists():
|
|
685
|
+
raise ValueError("root_not_found:%s" % args.root)
|
|
686
|
+
if not args.root.is_dir():
|
|
687
|
+
raise ValueError("root_is_not_directory:%s" % args.root)
|
|
688
|
+
# Force a directory read here so an unreadable path fails before it can
|
|
689
|
+
# be mistaken for an empty new-analysis project.
|
|
690
|
+
next(args.root.iterdir(), None)
|
|
691
|
+
payload = inspect(args.root, args.expected_chapters)
|
|
692
|
+
if args.compact:
|
|
693
|
+
payload = compact_payload(payload)
|
|
694
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True))
|
|
695
|
+
return 0
|
|
696
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
697
|
+
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
|
698
|
+
return 2
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
if __name__ == "__main__":
|
|
702
|
+
raise SystemExit(main())
|