@oh-story/dsh 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -12
- package/lib/client.js +2970 -537
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +218 -178
- package/lib/drama/skills/short-drama/SKILL.md +110 -117
- package/lib/drama/skills/short-drama/references/audience-reveal.md +14 -67
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +26 -77
- package/lib/drama/skills/short-drama/references/creator-documents.md +204 -0
- package/lib/drama/skills/short-drama/references/creator-workflow.md +73 -63
- package/lib/drama/skills/short-drama/references/knowhow-index.md +5 -0
- package/lib/drama/skills/short-drama/references/look-development.md +2 -3
- package/lib/drama/skills/short-drama/references/pickup-and-alternate.md +14 -48
- package/lib/drama/skills/short-drama/references/production-form-profiles.md +1 -1
- package/lib/drama/skills/short-drama/references/reference-roles.md +32 -135
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +16 -19
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
- package/lib/drama/skills/short-drama-assets/SKILL.md +59 -170
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +18 -25
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +13 -15
- package/lib/drama/skills/short-drama-assets/references/continuity-delta.md +11 -14
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
- package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +6 -12
- package/lib/drama/skills/short-drama-assets/references/occurrence-extraction.md +8 -11
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +21 -2
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +12 -105
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +12 -32
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +45 -155
- package/lib/drama/skills/short-drama-image-prompts/references/character-and-look.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +12 -13
- package/lib/drama/skills/short-drama-image-prompts/references/edit-and-revision.md +39 -51
- package/lib/drama/skills/short-drama-image-prompts/references/location-plate.md +3 -4
- package/lib/drama/skills/short-drama-image-prompts/references/look-and-state-variant.md +15 -14
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +11 -15
- package/lib/drama/skills/short-drama-image-prompts/references/production-sheet-recipes.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/prop-plate.md +4 -7
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +23 -23
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +8 -65
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +67 -15
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
- package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
- package/lib/drama/skills/short-drama-review/SKILL.md +61 -123
- package/lib/drama/skills/short-drama-review/references/anti-template-repair.md +10 -15
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +15 -16
- package/lib/drama/skills/short-drama-review/references/project-calibration.md +7 -9
- package/lib/drama/skills/short-drama-review/references/review-method.md +8 -9
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +9 -13
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +104 -4
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +27 -19
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +4 -63
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +116 -238
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +9 -34
- package/lib/drama/skills/short-drama-storyboard/references/comic-keyframe-lexicon.md +156 -0
- package/lib/drama/skills/short-drama-storyboard/references/coverage-audition.md +5 -10
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +12 -7
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
- package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +27 -62
- package/lib/drama/skills/short-drama-storyboard/references/scene-visual-plan.md +16 -25
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +21 -23
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +8 -10
- package/lib/drama/skills/short-drama-storyboard/references/shot-revision-identity.md +9 -13
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +16 -71
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +123 -230
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
- package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +10 -8
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +2 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +35 -27
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +8 -11
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +13 -10
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +34 -63
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +17 -73
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +74 -268
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +7 -0
- package/lib/drama/skills/short-drama-write/references/scene-handoff-capsule.md +17 -43
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +15 -15
- package/lib/drama/skills/short-drama-write/references/script-craft.md +208 -21
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +11 -70
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
- package/lib/index.js +971 -46
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/LICENSE +21 -0
- package/lib/novel-to-game/README.md +180 -0
- package/lib/novel-to-game/README_ZH.md +178 -0
- package/lib/novel-to-game/VERSION +1 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
- package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
- package/lib/novel-to-game/manifest.json +508 -0
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
- package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
- package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
- package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
- package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
- package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
- package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
- package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
- package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
- package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
- package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
- package/lib/oh-story/manifest.json +419 -309
- package/lib/oh-story/roles/character-designer.md +4 -4
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +41 -42
- package/lib/oh-story/roles/story-architect.md +19 -23
- package/lib/oh-story/roles/story-explorer.md +8 -5
- package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +16 -9
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-import/SKILL.md +21 -8
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
- package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
- package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/story-long-write/references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-long-write/references/{reversal-toolkit.md → long-reversal.md} +18 -32
- package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +5 -4
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +17 -11
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-review/SKILL.md +26 -17
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +85 -41
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
- package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/story-setup/references/agent-references/{genre-core-mechanics.md → long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
- package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-setup/references/agent-references/long-reversal.md} +18 -32
- package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
- package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
- package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → short-reversal.md} +29 -23
- package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
- 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/writing-craft.md +55 -55
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
- package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
- package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
- package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
- package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
- package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
- package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -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 +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
- 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 +6 -6
- 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 +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
- 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 +32 -44
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
- package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
- package/lib/types/client/drama-production-view.d.ts +34 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -0
- package/lib/types/client/drama-production.d.ts +92 -0
- package/lib/types/client/drama-production.d.ts.map +1 -0
- package/lib/types/client/file-activity.d.ts +9 -2
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-intents.d.ts +9 -0
- package/lib/types/client/production-intents.d.ts.map +1 -0
- package/lib/types/client/production-prompts.d.ts +9 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -0
- package/lib/types/client/production-runtime.d.ts +58 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -0
- package/lib/types/client/video-studio.d.ts +41 -0
- package/lib/types/client/video-studio.d.ts.map +1 -0
- package/lib/types/client/workbench-ui.d.ts +5 -0
- package/lib/types/client/workbench-ui.d.ts.map +1 -0
- package/lib/types/game-verification.d.ts +16 -0
- package/lib/types/game-verification.d.ts.map +1 -0
- package/lib/types/index.d.ts +5 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/production-intent.d.ts +21 -0
- package/lib/types/production-intent.d.ts.map +1 -0
- package/lib/types/production-tool.d.ts +5 -0
- package/lib/types/production-tool.d.ts.map +1 -0
- package/lib/types/reference-tool.d.ts +6 -0
- package/lib/types/reference-tool.d.ts.map +1 -0
- package/lib/types/role-provider.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts +3 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts +7 -0
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts +42 -0
- package/lib/types/video-project.d.ts.map +1 -0
- package/lib/types/workspace-request-trust.d.ts +6 -0
- package/lib/types/workspace-request-trust.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts +20 -0
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +282 -0
- package/lib/video-recap/LICENSE +21 -0
- package/lib/video-recap/README.en.md +257 -0
- package/lib/video-recap/README.md +255 -0
- package/lib/video-recap/manifest.json +684 -0
- package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
- package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
- package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
- package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
- package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
- package/lib/video-recap/skills/video-script/SKILL.md +248 -0
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
- package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
- package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
- package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
- package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
- package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
- package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
- package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
- package/package.json +57 -41
- package/lib/drama/skills/short-drama/references/lifecycle-commands.md +0 -130
- package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
- package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
- package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
- package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
- package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
- package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
- package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
- /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
- /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
|
@@ -13,6 +13,7 @@ Detect high-risk AI-flavor prose patterns that need human rewrite:
|
|
|
13
13
|
- repeated negative setup followed by positive flip
|
|
14
14
|
- em-dash (按功能改写), 碎句号 (连续短叙述句), 长段落 (按镜头断段)
|
|
15
15
|
- 微动作复读 (「了下/了一下」式轻量补语高密度,电报体指纹)
|
|
16
|
+
- 套式反应细节 (指尖/指节/目光等无功能微动作与「平静得像在念」式语气比喻成片)
|
|
16
17
|
- 抽象总结复读 (命运/棋局/这一刻终于明白/才刚刚开始,AI 结尾腔)
|
|
17
18
|
- 套词密度过高 (仿佛/一丝/深吸一口气/平静无波等禁用词聚集)
|
|
18
19
|
- 比喻密度过高 (像/好像/仿佛/如同等比喻标记成片复现)
|
|
@@ -30,7 +31,7 @@ Detect high-risk AI-flavor prose patterns that need human rewrite:
|
|
|
30
31
|
- 引号强调滥用 (叙述里 1-4 字短词加引号强调,密度型)
|
|
31
32
|
|
|
32
33
|
Each finding carries severity: blocking by default for generation/deslop cleanup (not-is-comparison / em-dash / voice-contrast / negation-parade / reverse-not-is / trailer-ending / trailer-summary). This is a local style/readability gate, not an AIGC detector score; functional human text can be marked for review instead of hard-edited for a detector.
|
|
33
|
-
或 advisory (period-stutter / long-paragraph / micro-action-tic / action-list-tic / abstract-summary-tic / cliche-density-tic / metaphor-density-tic / reasoning-chain-tic / system-notice-formality-tic / overcompressed-prose-tic / low-connective-density-tic / quote-emphasis-tic / formulaic-parallelism,是提示,justified 的长推理/氛围段可保留)。
|
|
34
|
+
或 advisory (period-stutter / long-paragraph / micro-action-tic / stock-reaction-tic / action-list-tic / abstract-summary-tic / cliche-density-tic / metaphor-density-tic / reasoning-chain-tic / system-notice-formality-tic / overcompressed-prose-tic / low-connective-density-tic / quote-emphasis-tic / formulaic-parallelism,是提示,justified 的长推理/氛围段可保留)。
|
|
34
35
|
--fail-on=blocking 只在出现 blocking finding 时退出 1;默认 --fail-on=all 有任何 finding 即退出 1。
|
|
35
36
|
|
|
36
37
|
The script reports findings only. It never rewrites text, because the safe fix is
|
|
@@ -57,6 +58,26 @@ const MICRO_TIC_PATTERN = /了(?:[一两三几半])?[下阵圈道声眼口气会
|
|
|
57
58
|
const MICRO_TIC_MIN_HITS = 5;
|
|
58
59
|
const MICRO_TIC_PER_KILO = 6;
|
|
59
60
|
|
|
61
|
+
// 套式反应细节:不是禁写身体,而是提示成片出现的“部位 + 轻微动作/状态”、
|
|
62
|
+
// “胸口像被撞了一下”、喉结/眼圈/声音放轻等通用情绪尾巴,以及“平静语气 +
|
|
63
|
+
// 像在念/宣判”模板。此类句子词面变化大,不能逐词 blocking;按章聚集到 4 处才
|
|
64
|
+
// advisory,要求逐处做删除测试。正常受伤、打斗、生理反应若承担物理后果可保留。
|
|
65
|
+
const STOCK_REACTION_PATTERNS = [
|
|
66
|
+
/(?:指尖|手指|指节|手背|掌心|拳头|袖口|衣角|裙角|下唇|嘴唇|唇角|嘴角|眉头|眼底|眸光|目光|视线|肩膀|呼吸)[^。!?!?\n]{0,16}(?:轻轻|微微|缓缓|悄然|不自觉|无意识|下意识|攥紧|握紧|收紧|绞紧|泛白|发白|叩|敲|摩挲|抿紧|抿成|移开|垂下|躲开|一颤|颤了?一下|停了?一下|顿了?一下)/g,
|
|
67
|
+
/(?:语气|声音)[^。!?!?\n]{0,12}(?:平静|冷静|平淡|冷淡|淡漠|平直)[^。!?!?\n]{0,12}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:念|读|报|说|陈述|宣判|背诵)/g,
|
|
68
|
+
/(?:胸口|心口)[^。!?!?\n]{0,16}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:撞|锤|压|攥|堵)[^。!?!?\n]{0,8}(?:一下|一记|一拳)?/g,
|
|
69
|
+
/(?:声音|嗓音|语气)[^。!?!?\n]{0,12}(?:放轻|压低|发紧|发颤|很轻|轻了些)/g,
|
|
70
|
+
/(?:喉结|喉头|喉咙)[^。!?!?\n]{0,10}(?:滚|动|紧|堵|发涩|发干)/g,
|
|
71
|
+
/(?:眼眶|眼圈|鼻子)[^。!?!?\n]{0,8}(?:发红|红了|发热|发酸|一酸)/g,
|
|
72
|
+
/(?:抿了?下唇|抿了?抿唇|抿了?下嘴|抿着笑)/g,
|
|
73
|
+
];
|
|
74
|
+
const STOCK_REACTION_MIN_HITS = 4;
|
|
75
|
+
// 校准(真人语料,<br> 已还原为换行):qimao 长篇 5584 章 + heiyan 短篇整篇 3983 篇。
|
|
76
|
+
// 长篇章尺度(中位约 2100 字)per-kilo 1.0→1.5 误报 0.43%→0.39%,几乎不动;
|
|
77
|
+
// 短篇整篇 8000-20000 字下 MIN_HITS 形同虚设、只剩密度门,1.0 时误报 5.57%,
|
|
78
|
+
// 1.5 降到 1.46%。故取 1.5,把两个总体拉到同一量级(四份副本共用一组阈值)。
|
|
79
|
+
const STOCK_REACTION_PER_KILO = 1.5;
|
|
80
|
+
|
|
60
81
|
// 监控摄像头式动作清单:同一段连续堆叠通用动作动词(伸手/拿起/取过/挑开/放下/转身等),
|
|
61
82
|
// 且用逗号/顿号串联成步骤表时,读感像无视角温度的监控记录。只做 advisory;
|
|
62
83
|
// 打斗/追逐等功能性动作编排可保留或人工复核。
|
|
@@ -406,6 +427,7 @@ function scanProsePatterns(proseLines) {
|
|
|
406
427
|
findings.push(...findQuoteEmphasisTic(proseLines));
|
|
407
428
|
findings.push(...findPeriodStutter(proseLines));
|
|
408
429
|
findings.push(...findMicroActionTic(proseLines));
|
|
430
|
+
findings.push(...findStockReactionTic(proseLines));
|
|
409
431
|
findings.push(...findActionListTic(proseLines));
|
|
410
432
|
findings.push(...findAbstractSummaryTic(proseLines));
|
|
411
433
|
findings.push(...findClicheDensityTic(proseLines));
|
|
@@ -700,6 +722,47 @@ function findMicroActionTic(proseLines) {
|
|
|
700
722
|
}];
|
|
701
723
|
}
|
|
702
724
|
|
|
725
|
+
// 套式反应细节:统计引号外叙述中通用的部位/声线反应与固定语气比喻。
|
|
726
|
+
// 这是删除测试的候选集,不是身体描写黑名单;全篇只报一条,保留有动作后果、
|
|
727
|
+
// 伤势、人物习惯或情节功能的细节。
|
|
728
|
+
function findStockReactionTic(proseLines) {
|
|
729
|
+
let hits = 0;
|
|
730
|
+
let narrativeChars = 0;
|
|
731
|
+
let firstLine = null;
|
|
732
|
+
const samples = [];
|
|
733
|
+
|
|
734
|
+
for (const { text, lineNo } of proseLines) {
|
|
735
|
+
const trimmed = text.trim();
|
|
736
|
+
if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
|
|
737
|
+
const narrative = stripQuoted(trimmed);
|
|
738
|
+
narrativeChars += visibleLength(narrative);
|
|
739
|
+
|
|
740
|
+
for (const pattern of STOCK_REACTION_PATTERNS) {
|
|
741
|
+
pattern.lastIndex = 0;
|
|
742
|
+
let match;
|
|
743
|
+
while ((match = pattern.exec(narrative)) !== null) {
|
|
744
|
+
hits += 1;
|
|
745
|
+
if (firstLine === null) firstLine = lineNo;
|
|
746
|
+
const sample = sentenceAround(narrative, match.index);
|
|
747
|
+
if (samples.length < 6 && sample && !samples.includes(sample)) samples.push(sample);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (narrativeChars === 0 || hits < STOCK_REACTION_MIN_HITS) return [];
|
|
753
|
+
const perKilo = (hits / narrativeChars) * 1000;
|
|
754
|
+
if (perKilo < STOCK_REACTION_PER_KILO) return [];
|
|
755
|
+
|
|
756
|
+
return [{
|
|
757
|
+
line: firstLine,
|
|
758
|
+
column: 1,
|
|
759
|
+
type: 'stock-reaction-tic',
|
|
760
|
+
severity: 'advisory',
|
|
761
|
+
message: `套式反应细节:指尖/指节/喉结/眼圈/声音放轻等通用反应或“平静得像在念”式语气比喻 ${hits} 处(${perKilo.toFixed(1)}/千字);逐处做删除测试,只标注情绪、不改变选择、关系、物件或动作结果的删掉,不要换部位或同义动作。`,
|
|
762
|
+
excerpt: compact(samples.join(' | ')),
|
|
763
|
+
}];
|
|
764
|
+
}
|
|
765
|
+
|
|
703
766
|
function findActionListTic(proseLines) {
|
|
704
767
|
const findings = [];
|
|
705
768
|
|
|
@@ -3,25 +3,40 @@
|
|
|
3
3
|
|
|
4
4
|
The language model supplies compact semantic JSON. This tool validates and
|
|
5
5
|
merges that input in memory, renders every derived view, then atomically writes
|
|
6
|
-
``_tracking-state.json`` last as the single commit point.
|
|
7
|
-
|
|
6
|
+
``_tracking-state.json`` last as the single commit point. Per-project locking
|
|
7
|
+
serializes concurrent writers before revision and wordcount checks.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
13
|
import copy
|
|
14
|
+
import importlib.util
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
17
18
|
import stat
|
|
18
19
|
import sys
|
|
19
20
|
import tempfile
|
|
21
|
+
import time
|
|
20
22
|
import unicodedata
|
|
23
|
+
from contextlib import contextmanager
|
|
21
24
|
from pathlib import Path
|
|
22
25
|
from typing import Any
|
|
23
26
|
|
|
24
27
|
|
|
28
|
+
_WORDCOUNT_CORE_PATH = Path(__file__).with_name("wordcount_core.py")
|
|
29
|
+
if not _WORDCOUNT_CORE_PATH.is_file(): # pragma: no cover - broken deployment
|
|
30
|
+
raise RuntimeError("TOOL_UNAVAILABLE: wordcount_core.py")
|
|
31
|
+
_WORDCOUNT_CORE_SPEC = importlib.util.spec_from_file_location(
|
|
32
|
+
"story_wordcount_core", _WORDCOUNT_CORE_PATH
|
|
33
|
+
)
|
|
34
|
+
if _WORDCOUNT_CORE_SPEC is None or _WORDCOUNT_CORE_SPEC.loader is None: # pragma: no cover
|
|
35
|
+
raise RuntimeError("unable to load wordcount core")
|
|
36
|
+
wordcount_core = importlib.util.module_from_spec(_WORDCOUNT_CORE_SPEC)
|
|
37
|
+
_WORDCOUNT_CORE_SPEC.loader.exec_module(wordcount_core)
|
|
38
|
+
|
|
39
|
+
|
|
25
40
|
INPUT_SCHEMA_VERSION = 1
|
|
26
41
|
TRACKING_SCHEMA_VERSION = 4
|
|
27
42
|
DELTA_TARGET_BYTES = 1536
|
|
@@ -74,6 +89,13 @@ def require(condition: bool, message: str) -> None:
|
|
|
74
89
|
raise TrackingError(message)
|
|
75
90
|
|
|
76
91
|
|
|
92
|
+
def wordcount_value(function: Any, *args: Any, **kwargs: Any) -> Any:
|
|
93
|
+
try:
|
|
94
|
+
return function(*args, **kwargs)
|
|
95
|
+
except wordcount_core.WordcountError as exc:
|
|
96
|
+
raise TrackingError(str(exc)) from exc
|
|
97
|
+
|
|
98
|
+
|
|
77
99
|
def as_mapping(value: object, label: str) -> dict[str, Any]:
|
|
78
100
|
require(isinstance(value, dict), f"{label} must be a JSON object")
|
|
79
101
|
return value
|
|
@@ -188,6 +210,34 @@ def state_path(project: Path) -> Path:
|
|
|
188
210
|
return tracking_root(project) / "_tracking-state.json"
|
|
189
211
|
|
|
190
212
|
|
|
213
|
+
@contextmanager
|
|
214
|
+
def project_write_lock(project: Path, *, timeout_seconds: float = 10.0):
|
|
215
|
+
tracking = tracking_root(project)
|
|
216
|
+
tracking.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
path = tracking / ".tracking-commit.lock"
|
|
218
|
+
deadline = time.monotonic() + timeout_seconds
|
|
219
|
+
descriptor: int | None = None
|
|
220
|
+
while descriptor is None:
|
|
221
|
+
try:
|
|
222
|
+
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
|
|
223
|
+
except FileExistsError:
|
|
224
|
+
if time.monotonic() >= deadline:
|
|
225
|
+
raise TrackingError(
|
|
226
|
+
"tracking commit lock is busy or stale; retry, or remove 追踪/.tracking-commit.lock after confirming no commit is running"
|
|
227
|
+
)
|
|
228
|
+
time.sleep(0.05)
|
|
229
|
+
try:
|
|
230
|
+
os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
|
|
231
|
+
os.fsync(descriptor)
|
|
232
|
+
yield
|
|
233
|
+
finally:
|
|
234
|
+
os.close(descriptor)
|
|
235
|
+
try:
|
|
236
|
+
path.unlink()
|
|
237
|
+
except FileNotFoundError:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
191
241
|
def delta_path(tracking: Path, chapter: int) -> Path:
|
|
192
242
|
width = max(3, len(str(chapter)))
|
|
193
243
|
return tracking / "逐章记录" / f"第{chapter:0{width}d}章.md"
|
|
@@ -735,6 +785,18 @@ def render_delta(chapter: int, title: str, delta: dict[str, Any], core_names: se
|
|
|
735
785
|
return payload
|
|
736
786
|
|
|
737
787
|
|
|
788
|
+
def normalize_wordcount_records(value: object, last_chapter: int) -> dict[str, dict[str, Any]]:
|
|
789
|
+
records = as_mapping(value, "tracking state.wordcount_records")
|
|
790
|
+
normalized: dict[str, dict[str, Any]] = {}
|
|
791
|
+
for raw_chapter, raw_record in records.items():
|
|
792
|
+
require(isinstance(raw_chapter, str) and re.fullmatch(r"[1-9]\d*", raw_chapter) is not None,
|
|
793
|
+
"wordcount record chapter key is invalid")
|
|
794
|
+
chapter = int(raw_chapter)
|
|
795
|
+
require(chapter <= last_chapter, "wordcount record exceeds last committed chapter")
|
|
796
|
+
normalized[raw_chapter] = wordcount_value(wordcount_core.normalize_wordcount_record, raw_record)
|
|
797
|
+
return normalized
|
|
798
|
+
|
|
799
|
+
|
|
738
800
|
def normalize_state(document: object) -> dict[str, Any]:
|
|
739
801
|
root = as_mapping(document, "tracking state")
|
|
740
802
|
require_known_keys(
|
|
@@ -742,6 +804,7 @@ def normalize_state(document: object) -> dict[str, Any]:
|
|
|
742
804
|
{
|
|
743
805
|
"schema_version", "book_title", "last_committed_chapter", "imported_through_chapter",
|
|
744
806
|
"state_revision", "context", "characters", "foreshadow", "timeline",
|
|
807
|
+
"wordcount_records",
|
|
745
808
|
},
|
|
746
809
|
"tracking state",
|
|
747
810
|
)
|
|
@@ -766,16 +829,19 @@ def normalize_state(document: object) -> dict[str, Any]:
|
|
|
766
829
|
if last_chapter == 0:
|
|
767
830
|
require(not foreshadow, "a chapter-0 project cannot have planted foreshadow facts")
|
|
768
831
|
require(not timeline, "a chapter-0 project cannot have established timeline facts")
|
|
832
|
+
state_revision = as_int(root.get("state_revision"), "tracking state.state_revision")
|
|
833
|
+
wordcount_records = normalize_wordcount_records(root.get("wordcount_records", {}), last_chapter)
|
|
769
834
|
return {
|
|
770
835
|
"schema_version": TRACKING_SCHEMA_VERSION,
|
|
771
836
|
"book_title": clean_text(root.get("book_title"), "tracking state.book_title", max_bytes=240),
|
|
772
837
|
"last_committed_chapter": last_chapter,
|
|
773
838
|
"imported_through_chapter": imported_through,
|
|
774
|
-
"state_revision":
|
|
839
|
+
"state_revision": state_revision,
|
|
775
840
|
"context": context,
|
|
776
841
|
"characters": characters,
|
|
777
842
|
"foreshadow": foreshadow,
|
|
778
843
|
"timeline": timeline,
|
|
844
|
+
"wordcount_records": wordcount_records,
|
|
779
845
|
}
|
|
780
846
|
|
|
781
847
|
|
|
@@ -789,7 +855,10 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
|
|
|
789
855
|
root = as_mapping(document, "init input")
|
|
790
856
|
require_known_keys(
|
|
791
857
|
root,
|
|
792
|
-
{
|
|
858
|
+
{
|
|
859
|
+
"schema_version", "book_title", "last_chapter", "context", "character_snapshots",
|
|
860
|
+
"foreshadow", "timeline_events",
|
|
861
|
+
},
|
|
793
862
|
"init input",
|
|
794
863
|
)
|
|
795
864
|
require(root.get("schema_version") == INPUT_SCHEMA_VERSION, "init input schema_version is unsupported")
|
|
@@ -826,17 +895,18 @@ def normalize_initial_document(document: object) -> dict[str, Any]:
|
|
|
826
895
|
"characters": snapshots,
|
|
827
896
|
"foreshadow": foreshadow,
|
|
828
897
|
"timeline": timeline,
|
|
898
|
+
"wordcount_records": {},
|
|
829
899
|
}
|
|
830
900
|
)
|
|
831
901
|
|
|
832
902
|
|
|
833
|
-
def normalize_transaction(state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
903
|
+
def normalize_transaction(project: Path, state: dict[str, Any], document: object) -> dict[str, Any]:
|
|
834
904
|
root = as_mapping(document, "transaction")
|
|
835
905
|
require_known_keys(
|
|
836
906
|
root,
|
|
837
907
|
{
|
|
838
908
|
"schema_version", "mode", "chapter", "chapter_title", "expected_state_revision",
|
|
839
|
-
"delta", "context", "character_snapshots",
|
|
909
|
+
"delta", "context", "character_snapshots", "wordcount",
|
|
840
910
|
},
|
|
841
911
|
"transaction",
|
|
842
912
|
)
|
|
@@ -845,6 +915,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
845
915
|
require(mode in {"append", "revision"}, "mode must be append or revision")
|
|
846
916
|
chapter = as_int(root.get("chapter"), "chapter", minimum=1)
|
|
847
917
|
expected_revision = as_int(root.get("expected_state_revision"), "expected_state_revision")
|
|
918
|
+
wordcount_input = root.get("wordcount")
|
|
848
919
|
require(expected_revision == state["state_revision"], "tracking state changed since this transaction was prepared")
|
|
849
920
|
last = state["last_committed_chapter"]
|
|
850
921
|
if mode == "append":
|
|
@@ -864,6 +935,11 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
864
935
|
snapshots=snapshots,
|
|
865
936
|
existing_core_names=existing_names,
|
|
866
937
|
)
|
|
938
|
+
wordcount = None
|
|
939
|
+
if wordcount_input is not None:
|
|
940
|
+
wordcount = wordcount_value(
|
|
941
|
+
wordcount_core.validate_current_wordcount_record, project, chapter, wordcount_input
|
|
942
|
+
)
|
|
867
943
|
return {
|
|
868
944
|
"mode": mode,
|
|
869
945
|
"chapter": chapter,
|
|
@@ -871,6 +947,7 @@ def normalize_transaction(state: dict[str, Any], document: object) -> dict[str,
|
|
|
871
947
|
"delta": delta,
|
|
872
948
|
"context": context,
|
|
873
949
|
"snapshots": snapshots,
|
|
950
|
+
"wordcount": wordcount,
|
|
874
951
|
}
|
|
875
952
|
|
|
876
953
|
|
|
@@ -891,6 +968,8 @@ def merge_transaction(state: dict[str, Any], transaction: dict[str, Any]) -> dic
|
|
|
891
968
|
next_state["last_committed_chapter"] = chapter
|
|
892
969
|
next_state["state_revision"] += 1
|
|
893
970
|
next_state["characters"].update(transaction["snapshots"])
|
|
971
|
+
if transaction["wordcount"] is not None:
|
|
972
|
+
next_state["wordcount_records"][str(chapter)] = transaction["wordcount"]
|
|
894
973
|
|
|
895
974
|
next_context = transaction["context"]
|
|
896
975
|
# 退役说的是「从此刻起离开当前状态」,只有 append 的逐章记录代表此刻;
|
|
@@ -1013,7 +1092,7 @@ def warn_sizes(views: dict[str, str], delta_payload: str | None = None) -> None:
|
|
|
1013
1092
|
)
|
|
1014
1093
|
|
|
1015
1094
|
|
|
1016
|
-
def
|
|
1095
|
+
def _initialize_locked(project: Path, document: object) -> dict[str, Any]:
|
|
1017
1096
|
tracking = tracking_root(project)
|
|
1018
1097
|
require(not state_path(project).exists(), "tracking state already exists; init never overwrites project state")
|
|
1019
1098
|
state = normalize_initial_document(document)
|
|
@@ -1036,11 +1115,16 @@ def initialize(project: Path, document: object) -> dict[str, Any]:
|
|
|
1036
1115
|
return state
|
|
1037
1116
|
|
|
1038
1117
|
|
|
1039
|
-
def
|
|
1118
|
+
def initialize(project: Path, document: object) -> dict[str, Any]:
|
|
1119
|
+
with project_write_lock(project):
|
|
1120
|
+
return _initialize_locked(project, document)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _apply_transaction_locked(project: Path, document: object) -> dict[str, Any]:
|
|
1040
1124
|
tracking = tracking_root(project)
|
|
1041
1125
|
require_no_retired_tracking_paths(tracking)
|
|
1042
1126
|
state = load_state(project)
|
|
1043
|
-
transaction = normalize_transaction(state, document)
|
|
1127
|
+
transaction = normalize_transaction(project, state, document)
|
|
1044
1128
|
next_state = merge_transaction(state, transaction)
|
|
1045
1129
|
|
|
1046
1130
|
delta_payload = render_delta(
|
|
@@ -1067,6 +1151,11 @@ def apply_transaction(project: Path, document: object) -> dict[str, Any]:
|
|
|
1067
1151
|
return next_state
|
|
1068
1152
|
|
|
1069
1153
|
|
|
1154
|
+
def apply_transaction(project: Path, document: object) -> dict[str, Any]:
|
|
1155
|
+
with project_write_lock(project):
|
|
1156
|
+
return _apply_transaction_locked(project, document)
|
|
1157
|
+
|
|
1158
|
+
|
|
1070
1159
|
def check_project(project: Path) -> dict[str, Any]:
|
|
1071
1160
|
tracking = tracking_root(project)
|
|
1072
1161
|
require_no_retired_tracking_paths(tracking)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Small deterministic wordcount core shared by storyctl and tracking."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
WORDCOUNT_SCHEMA = "story-wordcount-result/v1"
|
|
13
|
+
MEASUREMENT_SCHEMA = "story-wordcount-measurement/v1"
|
|
14
|
+
CHECKPOINT_SCHEMA = "story-wordcount-checkpoint/v1"
|
|
15
|
+
METRIC = "visible_chars_v1"
|
|
16
|
+
RESOLUTIONS = frozenset({"within_user_band", "accepted_current_length"})
|
|
17
|
+
|
|
18
|
+
_WHITE_SPACE_CODEPOINTS = frozenset(
|
|
19
|
+
[*range(0x0009, 0x000E)]
|
|
20
|
+
+ [
|
|
21
|
+
0x0020, 0x0085, 0x00A0, 0x1680, *range(0x2000, 0x200B),
|
|
22
|
+
0x2028, 0x2029, 0x202F, 0x205F, 0x3000,
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
_FRONTMATTER_KEY_RE = re.compile(r"^[A-Za-z_\u3400-\u9FFF][^:\n]{0,80}:[ \t]*(?:.*)$")
|
|
26
|
+
_LEADING_BLANK_RE = re.compile(r"^[\u0009\u0020\u3000]*$")
|
|
27
|
+
_ATX_HEADING_RE = re.compile(r"^[\u0009\u0020]{0,3}#{1,6}[\u0009\u0020]+\S")
|
|
28
|
+
_POSITIVE_INTEGER_RE = re.compile(r"^[1-9]\d*$")
|
|
29
|
+
_TARGET_LINE_RE = re.compile(r"^[ \t>*-]*字数目标[ \t]*[::][ \t]*([1-9]\d*)[ \t]*(?:字)?[ \t]*$", re.MULTILINE)
|
|
30
|
+
_METRIC_LINE_RE = re.compile(r"^[ \t>*-]*字数口径[ \t]*[::][ \t]*([A-Za-z0-9_-]+)[ \t]*$", re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WordcountError(ValueError):
|
|
34
|
+
"""Expected deterministic wordcount contract failure."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def require(condition: bool, message: str) -> None:
|
|
38
|
+
if not condition:
|
|
39
|
+
raise WordcountError(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def normalize_newlines(value: str) -> str:
|
|
43
|
+
return value.replace("\r\n", "\n").replace("\r", "\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def strip_recognizable_frontmatter(value: str) -> str:
|
|
47
|
+
lines = value.split("\n")
|
|
48
|
+
if not lines or lines[0] != "---":
|
|
49
|
+
return value
|
|
50
|
+
closing = next((index for index in range(1, min(len(lines), 201)) if lines[index] in {"---", "..."}), -1)
|
|
51
|
+
if closing < 2 or not any(_FRONTMATTER_KEY_RE.match(line) for line in lines[1:closing]):
|
|
52
|
+
return value
|
|
53
|
+
return "\n".join(lines[closing + 1 :])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def visible_body(value: str) -> str:
|
|
57
|
+
if not isinstance(value, str):
|
|
58
|
+
raise TypeError("body must be a string")
|
|
59
|
+
text = normalize_newlines(value)
|
|
60
|
+
if text.startswith("\ufeff"):
|
|
61
|
+
text = text[1:]
|
|
62
|
+
lines = strip_recognizable_frontmatter(text).split("\n")
|
|
63
|
+
while lines and _LEADING_BLANK_RE.match(lines[0]):
|
|
64
|
+
lines.pop(0)
|
|
65
|
+
if lines and _ATX_HEADING_RE.match(lines[0]):
|
|
66
|
+
lines.pop(0)
|
|
67
|
+
return "\n".join(lines)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def count_visible_chars(value: str) -> int:
|
|
71
|
+
return sum(ord(character) not in _WHITE_SPACE_CODEPOINTS for character in visible_body(value))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_target(value: Any) -> int:
|
|
75
|
+
raw = str(value if value is not None else "")
|
|
76
|
+
if not _POSITIVE_INTEGER_RE.fullmatch(raw):
|
|
77
|
+
raise WordcountError("target must be a positive integer")
|
|
78
|
+
target = int(raw)
|
|
79
|
+
require(target <= 9_007_199_254_740_991, "target exceeds Number.MAX_SAFE_INTEGER")
|
|
80
|
+
return target
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def compute_wordcount_bands(value: Any) -> dict[str, dict[str, int]]:
|
|
84
|
+
target = parse_target(value)
|
|
85
|
+
return {
|
|
86
|
+
"internal": {"min": (target * 88 + 99) // 100, "max": target * 112 // 100},
|
|
87
|
+
"user": {"min": (target * 85 + 99) // 100, "max": target * 115 // 100},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def invalid_wordcount_result(
|
|
92
|
+
reason: str, *, chapter: Any = None, case_id: Any = None,
|
|
93
|
+
target: int | None = None, actual: int | None = None,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
return {
|
|
96
|
+
"schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
97
|
+
"target": target, "actual": actual, "internal_band": None, "user_band": None,
|
|
98
|
+
"signed_error_pct": None, "absolute_error_pct": None,
|
|
99
|
+
"status": "invalid", "invalid_reason": reason,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def measure_wordcount(value: str, *, chapter: Any = None, case_id: Any = None) -> dict[str, Any]:
|
|
104
|
+
try:
|
|
105
|
+
actual = count_visible_chars(value)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
return {
|
|
108
|
+
"schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
|
|
109
|
+
"case_id": case_id, "actual": None, "status": "invalid", "invalid_reason": "INVALID_BODY",
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
"schema": MEASUREMENT_SCHEMA, "metric": METRIC, "chapter": chapter,
|
|
113
|
+
"case_id": case_id, "actual": actual, "status": "measured", "invalid_reason": None,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def evaluate_wordcount(
|
|
118
|
+
value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
try:
|
|
121
|
+
target = parse_target(target_value)
|
|
122
|
+
except (TypeError, ValueError):
|
|
123
|
+
return invalid_wordcount_result("INVALID_TARGET", chapter=chapter, case_id=case_id)
|
|
124
|
+
try:
|
|
125
|
+
actual = count_visible_chars(value)
|
|
126
|
+
except (TypeError, ValueError):
|
|
127
|
+
return invalid_wordcount_result("INVALID_BODY", chapter=chapter, case_id=case_id, target=target)
|
|
128
|
+
if actual == 0:
|
|
129
|
+
return invalid_wordcount_result("EMPTY_BODY", chapter=chapter, case_id=case_id, target=target, actual=actual)
|
|
130
|
+
bands = compute_wordcount_bands(target)
|
|
131
|
+
internal_pass = bands["internal"]["min"] <= actual <= bands["internal"]["max"]
|
|
132
|
+
user_pass = bands["user"]["min"] <= actual <= bands["user"]["max"]
|
|
133
|
+
status = "internal_pass" if internal_pass else (
|
|
134
|
+
"borderline" if user_pass else ("under" if actual < bands["user"]["min"] else "over")
|
|
135
|
+
)
|
|
136
|
+
signed_error = (actual - target) / target
|
|
137
|
+
return {
|
|
138
|
+
"schema": WORDCOUNT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
139
|
+
"target": target, "actual": actual,
|
|
140
|
+
"internal_band": {**bands["internal"], "status": "pass" if internal_pass else "fail"},
|
|
141
|
+
"user_band": {**bands["user"], "status": "pass" if user_pass else "fail"},
|
|
142
|
+
"signed_error_pct": signed_error, "absolute_error_pct": abs(signed_error),
|
|
143
|
+
"status": status, "invalid_reason": None,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def checkpoint_wordcount(
|
|
148
|
+
value: str, target_value: Any, *, chapter: Any = None, case_id: Any = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
target = parse_target(target_value)
|
|
151
|
+
actual = count_visible_chars(value)
|
|
152
|
+
user = compute_wordcount_bands(target)["user"]
|
|
153
|
+
return {
|
|
154
|
+
"schema": CHECKPOINT_SCHEMA, "metric": METRIC, "chapter": chapter, "case_id": case_id,
|
|
155
|
+
"target": target, "actual": actual, "user_band": user,
|
|
156
|
+
"remaining_user_range": {
|
|
157
|
+
"min": max(0, user["min"] - actual),
|
|
158
|
+
"max": max(0, user["max"] - actual),
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def target_from_outline(value: str) -> int:
|
|
164
|
+
text = normalize_newlines(value)
|
|
165
|
+
if text.startswith("\ufeff"):
|
|
166
|
+
text = text[1:]
|
|
167
|
+
targets = list(dict.fromkeys(_TARGET_LINE_RE.findall(text)))
|
|
168
|
+
metrics = list(dict.fromkeys(_METRIC_LINE_RE.findall(text)))
|
|
169
|
+
require(len(targets) == 1, "字数目标 must appear exactly once with one value")
|
|
170
|
+
require(metrics == [METRIC], f"字数口径 must appear exactly once as {METRIC}")
|
|
171
|
+
return parse_target(targets[0])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _chapter_number_from_name(name: str, *, outline: bool) -> int | None:
|
|
175
|
+
pattern = r"^细纲_第0*(\d+)章.*\.md$" if outline else r"^第0*(\d+)章(?:[_\- ].*)?\.md$"
|
|
176
|
+
match = re.match(pattern, name)
|
|
177
|
+
return int(match.group(1)) if match else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def find_chapter_file(directory: Path, chapter: int, *, outline: bool) -> Path:
|
|
181
|
+
require(isinstance(chapter, int) and not isinstance(chapter, bool) and chapter >= 1, "chapter must be >= 1")
|
|
182
|
+
require(directory.is_dir(), f"chapter directory is missing: {directory}")
|
|
183
|
+
matches = sorted(
|
|
184
|
+
path for path in directory.iterdir()
|
|
185
|
+
if path.is_file() and _chapter_number_from_name(path.name, outline=outline) == chapter
|
|
186
|
+
)
|
|
187
|
+
label = "outline" if outline else "body"
|
|
188
|
+
require(len(matches) == 1, f"chapter {chapter} must have exactly one {label} file")
|
|
189
|
+
return matches[0]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_project_wordcount_record(project: Path, chapter: int, *, resolution: str) -> dict[str, Any]:
|
|
193
|
+
require(resolution in RESOLUTIONS, f"unsupported wordcount resolution: {resolution}")
|
|
194
|
+
root = project.resolve()
|
|
195
|
+
outline_path = find_chapter_file(root / "大纲", chapter, outline=True)
|
|
196
|
+
body_path = find_chapter_file(root / "正文", chapter, outline=False)
|
|
197
|
+
try:
|
|
198
|
+
target = target_from_outline(outline_path.read_text(encoding="utf-8"))
|
|
199
|
+
body_bytes = body_path.read_bytes()
|
|
200
|
+
body = body_bytes.decode("utf-8")
|
|
201
|
+
except (OSError, UnicodeError) as exc:
|
|
202
|
+
raise WordcountError(f"unable to read chapter files: {exc}") from exc
|
|
203
|
+
result = evaluate_wordcount(body, target, chapter=chapter)
|
|
204
|
+
require(result["status"] != "invalid", f"invalid chapter wordcount: {result['invalid_reason']}")
|
|
205
|
+
in_user_band = result["status"] in {"internal_pass", "borderline"}
|
|
206
|
+
require(
|
|
207
|
+
(resolution == "within_user_band" and in_user_band)
|
|
208
|
+
or (resolution == "accepted_current_length" and not in_user_band),
|
|
209
|
+
"wordcount resolution does not match the current length",
|
|
210
|
+
)
|
|
211
|
+
return {
|
|
212
|
+
"metric": METRIC,
|
|
213
|
+
"target": result["target"],
|
|
214
|
+
"actual": result["actual"],
|
|
215
|
+
"status": result["status"],
|
|
216
|
+
"resolution": resolution,
|
|
217
|
+
"body_sha256": hashlib.sha256(body_bytes).hexdigest(),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def normalize_wordcount_record(value: object) -> dict[str, Any]:
|
|
222
|
+
require(isinstance(value, dict), "wordcount record must be an object")
|
|
223
|
+
require(
|
|
224
|
+
set(value) == {"metric", "target", "actual", "status", "resolution", "body_sha256"},
|
|
225
|
+
"wordcount record fields are invalid",
|
|
226
|
+
)
|
|
227
|
+
require(value.get("metric") == METRIC, "wordcount metric is unsupported")
|
|
228
|
+
target = parse_target(value.get("target"))
|
|
229
|
+
actual = value.get("actual")
|
|
230
|
+
require(isinstance(actual, int) and not isinstance(actual, bool) and actual >= 1, "wordcount actual is invalid")
|
|
231
|
+
expected = evaluate_wordcount("字" * actual, target)
|
|
232
|
+
require(value.get("status") == expected["status"], "wordcount status does not match target and actual")
|
|
233
|
+
resolution = value.get("resolution")
|
|
234
|
+
require(resolution in RESOLUTIONS, "wordcount resolution is unsupported")
|
|
235
|
+
in_user_band = expected["status"] in {"internal_pass", "borderline"}
|
|
236
|
+
require((resolution == "within_user_band") == in_user_band, "wordcount resolution does not match status")
|
|
237
|
+
digest = value.get("body_sha256")
|
|
238
|
+
require(isinstance(digest, str) and re.fullmatch(r"[0-9a-f]{64}", digest) is not None, "body_sha256 is invalid")
|
|
239
|
+
return dict(value)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_current_wordcount_record(project: Path, chapter: int, value: object) -> dict[str, Any]:
|
|
243
|
+
record = normalize_wordcount_record(value)
|
|
244
|
+
current = build_project_wordcount_record(project, chapter, resolution=record["resolution"])
|
|
245
|
+
require(record == current, "wordcount record is stale for the current body or target")
|
|
246
|
+
return record
|