@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,425 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render the character-relationship charts from 角色/角色关系.md.
|
|
3
|
+
|
|
4
|
+
The Markdown file ``人物关系图/人物关系图.md`` (Mermaid graphs plus a plain
|
|
5
|
+
list) is always written: it shows Chinese names in any Markdown viewer. PNG
|
|
6
|
+
pictures are optional (``--png``) and are drawn only when matplotlib and a font
|
|
7
|
+
that really contains every drawn character are available. Emoji and other
|
|
8
|
+
symbols are left out of the pictures; without a font for the remaining text no
|
|
9
|
+
PNG is written, so names are never replaced by pinyin, initials or empty boxes.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import unicodedata
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Dict, List, Optional, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
RELATION_FILE = Path("角色") / "角色关系.md"
|
|
29
|
+
CHART_DIR = Path("人物关系图")
|
|
30
|
+
CHART_MD = CHART_DIR / "人物关系图.md"
|
|
31
|
+
CORE_PNG = CHART_DIR / "核心人物关系.png"
|
|
32
|
+
EVOLUTION_PNG = CHART_DIR / "关键关系演变.png"
|
|
33
|
+
# 「甲 → 乙」, 「甲 ↔ 乙」 (both ways) and chains such as 「甲 → 乙 → 丙」.
|
|
34
|
+
ARROW_RE = re.compile(r"\s*(↔|<->|⇔|<=>|→|->|⇒|=>|—>|->)\s*")
|
|
35
|
+
BOTH_WAYS = {"↔", "<->", "⇔", "<=>"}
|
|
36
|
+
# Common Simplified-Chinese capable fonts on macOS, Windows and Linux.
|
|
37
|
+
CJK_FONT_NAMES = (
|
|
38
|
+
"PingFang SC", "Hiragino Sans GB", "Heiti SC", "STHeiti", "Songti SC", "STSong",
|
|
39
|
+
"Microsoft YaHei", "SimHei", "SimSun", "DengXian",
|
|
40
|
+
"Noto Sans CJK SC", "Noto Sans SC", "Noto Serif CJK SC", "Source Han Sans SC",
|
|
41
|
+
"Source Han Sans CN", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Sarasa Gothic SC",
|
|
42
|
+
"Arial Unicode MS",
|
|
43
|
+
)
|
|
44
|
+
# Fonts that answer every character with a placeholder box; they never count.
|
|
45
|
+
FALLBACK_FONT_RE = re.compile(r"last\s*resort|adobe\s*blank", re.IGNORECASE)
|
|
46
|
+
BASE_TEXT = "中文人物关系未命名→…"
|
|
47
|
+
MD_VIEW = "关系图已经写在 人物关系图/人物关系图.md 里,用能预览 Markdown 的编辑器打开就能看到中文。"
|
|
48
|
+
NO_FONT_MESSAGE = (
|
|
49
|
+
"这台电脑上没有找到能显示中文的字体,所以没有生成关系图图片(硬画会把人名变成拼音或方块)。"
|
|
50
|
+
+ MD_VIEW + "装上任意一款中文字体(如思源黑体、文泉驿)后再生成一次,就能得到图片版。"
|
|
51
|
+
)
|
|
52
|
+
MISSING_GLYPH_MESSAGE = (
|
|
53
|
+
"这台电脑上的中文字体显示不了关系图里的「%s」,所以没有生成图片(硬画会变成方块)。"
|
|
54
|
+
+ MD_VIEW + "装上字更全的中文字体(如思源黑体)后再生成一次,就能得到图片版。"
|
|
55
|
+
)
|
|
56
|
+
PNG_FAILED_MESSAGE = "关系图图片这次没画成。" + MD_VIEW
|
|
57
|
+
NO_MATPLOTLIB_MESSAGE = "当前环境不能画图片," + MD_VIEW
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChartError(ValueError):
|
|
61
|
+
def __init__(self, code: str, author_message: str) -> None:
|
|
62
|
+
super().__init__(code)
|
|
63
|
+
self.code = code
|
|
64
|
+
self.author_message = author_message
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def atomic_write(path: Path, data: bytes) -> None:
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
fd, temporary = tempfile.mkstemp(prefix=".%s." % path.name, suffix=".tmp", dir=str(path.parent))
|
|
70
|
+
try:
|
|
71
|
+
with os.fdopen(fd, "wb") as handle:
|
|
72
|
+
handle.write(data)
|
|
73
|
+
os.replace(temporary, str(path))
|
|
74
|
+
except BaseException:
|
|
75
|
+
try:
|
|
76
|
+
os.unlink(temporary)
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cells(line: str) -> List[str]:
|
|
83
|
+
"""Table cells; an escaped 「\\|」 stays inside its cell."""
|
|
84
|
+
stripped = line.strip()
|
|
85
|
+
stripped = stripped[1:] if stripped.startswith("|") else stripped
|
|
86
|
+
stripped = stripped[:-1] if stripped.endswith("|") and not stripped.endswith("\\|") else stripped
|
|
87
|
+
return [cell.strip().replace("\\|", "|") for cell in re.split(r"(?<!\\)\|", stripped)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pick(row: Dict[str, str], *names: str) -> str:
|
|
91
|
+
for name in names:
|
|
92
|
+
for key, value in row.items():
|
|
93
|
+
if name in key and value and value not in {"-", "—", "无"}:
|
|
94
|
+
return value
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
SOURCE_KEYS = ("主体", "角色A", "人物A", "A", "甲")
|
|
99
|
+
TARGET_KEYS = ("客体", "角色B", "人物B", "B", "乙")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def exact(row: Dict[str, str], keys: Tuple[str, ...]) -> str:
|
|
103
|
+
for key in keys:
|
|
104
|
+
value = row.get(key, "")
|
|
105
|
+
if value and value not in {"-", "—"}:
|
|
106
|
+
return value
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def parse_relations(text: str) -> Tuple[List[Dict[str, str]], int]:
|
|
111
|
+
"""Relations of a relationship table, plus the number of rows left out.
|
|
112
|
+
|
|
113
|
+
Current files use one 「主体 → 客体」 column; older ones use separate
|
|
114
|
+
主体/客体 or A/B columns. Any table with such a pair of columns counts.
|
|
115
|
+
"""
|
|
116
|
+
relations = [] # type: List[Dict[str, str]]
|
|
117
|
+
skipped = 0
|
|
118
|
+
header = None # type: Optional[List[str]]
|
|
119
|
+
for line in text.splitlines():
|
|
120
|
+
stripped = line.strip()
|
|
121
|
+
if not stripped.startswith("|"):
|
|
122
|
+
header = None
|
|
123
|
+
continue
|
|
124
|
+
row_cells = cells(stripped)
|
|
125
|
+
if header is None:
|
|
126
|
+
names = set(row_cells)
|
|
127
|
+
if (any("主体" in cell and "客体" in cell for cell in row_cells) or "关系方向" in names
|
|
128
|
+
or (names & set(SOURCE_KEYS) and names & set(TARGET_KEYS))):
|
|
129
|
+
header = row_cells
|
|
130
|
+
continue
|
|
131
|
+
if not "".join(row_cells).strip("-: "):
|
|
132
|
+
continue
|
|
133
|
+
row = dict(zip(header, row_cells))
|
|
134
|
+
pair = next((value for key, value in row.items() if ("主体" in key and "客体" in key) or key == "关系方向"), "")
|
|
135
|
+
tokens = [part.strip() for part in ARROW_RE.split(pair)] if pair else []
|
|
136
|
+
names, arrows = tokens[0::2], tokens[1::2]
|
|
137
|
+
if len(names) < 2:
|
|
138
|
+
names = [exact(row, SOURCE_KEYS), exact(row, TARGET_KEYS)]
|
|
139
|
+
arrows = ["→"]
|
|
140
|
+
if len(names) < 2 or not all(names):
|
|
141
|
+
skipped += 1
|
|
142
|
+
continue
|
|
143
|
+
fields = {
|
|
144
|
+
"id": pick(row, "关系ID", "ID"),
|
|
145
|
+
"label": pick(row, "关系动作", "类型", "表面关系"),
|
|
146
|
+
"state": pick(row, "变化后状态", "真实关系", "表面关系"),
|
|
147
|
+
"trigger": pick(row, "触发事件", "触发"),
|
|
148
|
+
}
|
|
149
|
+
for index, arrow in enumerate(arrows):
|
|
150
|
+
ends = [(names[index], names[index + 1])]
|
|
151
|
+
if arrow in BOTH_WAYS:
|
|
152
|
+
ends.append((names[index + 1], names[index]))
|
|
153
|
+
for source, target in ends:
|
|
154
|
+
relations.append(dict(fields, source=source, target=target))
|
|
155
|
+
return relations, skipped
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def mermaid_text(value: str, placeholder: str) -> str:
|
|
159
|
+
value = re.sub(r"[`*]|%{2,}", "", value)
|
|
160
|
+
value = value.replace('"', "'").replace("|", "/").replace("\n", " ").strip()
|
|
161
|
+
return value or placeholder
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def latest_by_pair(relations: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
165
|
+
latest = {} # type: Dict[Tuple[str, str], Dict[str, str]]
|
|
166
|
+
for relation in relations:
|
|
167
|
+
latest[(relation["source"], relation["target"])] = relation
|
|
168
|
+
return list(latest.values())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def evolutions(relations: List[Dict[str, str]]) -> List[Tuple[Tuple[str, str], List[Dict[str, str]]]]:
|
|
172
|
+
history = {} # type: Dict[Tuple[str, str], List[Dict[str, str]]]
|
|
173
|
+
for relation in relations:
|
|
174
|
+
history.setdefault((relation["source"], relation["target"]), []).append(relation)
|
|
175
|
+
return [(pair, rows) for pair, rows in history.items() if len(rows) > 1]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def render_markdown(book: str, relations: List[Dict[str, str]], png_note: str) -> str:
|
|
179
|
+
names = [] # type: List[str]
|
|
180
|
+
for relation in relations:
|
|
181
|
+
for name in (relation["source"], relation["target"]):
|
|
182
|
+
if name not in names:
|
|
183
|
+
names.append(name)
|
|
184
|
+
node = {name: "p%s" % index for index, name in enumerate(names, start=1)}
|
|
185
|
+
lines = ["# 人物关系图:%s" % book, "",
|
|
186
|
+
"> 由 角色/角色关系.md 生成;关系事实以该文件为准。%s" % png_note, "",
|
|
187
|
+
"## 核心人物关系", "", "```mermaid", "graph LR"]
|
|
188
|
+
lines.extend(' %s["%s"]' % (node[name], mermaid_text(name, "未命名")) for name in names)
|
|
189
|
+
for relation in latest_by_pair(relations):
|
|
190
|
+
label = relation["state"] or relation["label"] or "关系"
|
|
191
|
+
lines.append(' %s -->|"%s"| %s' % (node[relation["source"]], mermaid_text(label, "关系"),
|
|
192
|
+
node[relation["target"]]))
|
|
193
|
+
lines.extend(["```", ""])
|
|
194
|
+
for relation in latest_by_pair(relations):
|
|
195
|
+
label = relation["state"] or relation["label"] or "关系"
|
|
196
|
+
lines.append("- %s → %s:%s" % (relation["source"], relation["target"], label))
|
|
197
|
+
lines.extend(["", "## 关键关系演变", ""])
|
|
198
|
+
changes = evolutions(relations)
|
|
199
|
+
if not changes:
|
|
200
|
+
lines.append("本书没有记录到同一对人物前后变化的关系。")
|
|
201
|
+
for (source, target), rows in changes:
|
|
202
|
+
steps = []
|
|
203
|
+
for row in rows:
|
|
204
|
+
state = row["state"] or row["label"] or "关系"
|
|
205
|
+
steps.append("%s(%s)" % (state, row["trigger"]) if row["trigger"] else state)
|
|
206
|
+
lines.append("- %s → %s:%s" % (source, target, " → ".join(steps)))
|
|
207
|
+
return "\n".join(lines) + "\n"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def png_text(value: str, placeholder: str = "") -> str:
|
|
211
|
+
"""Text as drawn in the pictures: emoji and other symbols are left out."""
|
|
212
|
+
kept = "".join(char for char in value
|
|
213
|
+
if char in BASE_TEXT or not unicodedata.category(char).startswith(("S", "C", "M")))
|
|
214
|
+
return re.sub(r"\s+", " ", kept).strip() or placeholder
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def font_face(path: str): # noqa: ANN201 - matplotlib type
|
|
218
|
+
from matplotlib.ft2font import FT2Font
|
|
219
|
+
return FT2Font(path)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def missing_glyphs(path: str, text: str) -> Optional[List[str]]:
|
|
223
|
+
"""Characters of text the font (face 0) lacks; None when the font is unusable."""
|
|
224
|
+
if FALLBACK_FONT_RE.search(os.path.basename(path)):
|
|
225
|
+
return None
|
|
226
|
+
try:
|
|
227
|
+
font = font_face(path)
|
|
228
|
+
if FALLBACK_FONT_RE.search(str(getattr(font, "family_name", ""))):
|
|
229
|
+
return None
|
|
230
|
+
return sorted({char for char in text if not char.isspace() and not font.get_char_index(ord(char))})
|
|
231
|
+
except Exception: # noqa: BLE001 - any font loading failure means "not usable"
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def font_candidates() -> List[str]:
|
|
236
|
+
"""Installed fonts worth trying, well-known Chinese fonts first."""
|
|
237
|
+
try:
|
|
238
|
+
from matplotlib import font_manager
|
|
239
|
+
except ImportError:
|
|
240
|
+
return []
|
|
241
|
+
by_name = {} # type: Dict[str, List[str]]
|
|
242
|
+
for entry in font_manager.fontManager.ttflist:
|
|
243
|
+
paths = by_name.setdefault(entry.name, [])
|
|
244
|
+
if entry.fname not in paths:
|
|
245
|
+
paths.append(entry.fname)
|
|
246
|
+
candidates = [path for name in CJK_FONT_NAMES for path in by_name.get(name, [])]
|
|
247
|
+
if shutil.which("fc-list"):
|
|
248
|
+
try:
|
|
249
|
+
listed = subprocess.run(["fc-list", ":lang=zh", "file"], capture_output=True, text=True,
|
|
250
|
+
timeout=10, check=False).stdout
|
|
251
|
+
except (OSError, subprocess.SubprocessError):
|
|
252
|
+
listed = ""
|
|
253
|
+
candidates.extend(line.split(":", 1)[0].strip() for line in listed.splitlines())
|
|
254
|
+
return candidates
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def find_cjk_font(text: str) -> Tuple[Optional[str], List[str]]:
|
|
258
|
+
"""(font that draws every character of text, []) or (None, characters no Chinese font draws)."""
|
|
259
|
+
text = text + BASE_TEXT
|
|
260
|
+
override = os.environ.get("STORY_ANALYZE_CHART_FONT")
|
|
261
|
+
if override == "none":
|
|
262
|
+
return None, []
|
|
263
|
+
fewest = None # type: Optional[List[str]]
|
|
264
|
+
seen = set()
|
|
265
|
+
for path in [override] if override else font_candidates():
|
|
266
|
+
if not path or path in seen:
|
|
267
|
+
continue
|
|
268
|
+
seen.add(path)
|
|
269
|
+
missing = missing_glyphs(path, text)
|
|
270
|
+
if missing is None or any(char in BASE_TEXT for char in missing):
|
|
271
|
+
continue
|
|
272
|
+
if not missing:
|
|
273
|
+
return path, []
|
|
274
|
+
if fewest is None or len(missing) < len(fewest):
|
|
275
|
+
fewest = missing
|
|
276
|
+
return None, fewest or []
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def matplotlib_available() -> bool:
|
|
280
|
+
try:
|
|
281
|
+
import matplotlib # noqa: F401
|
|
282
|
+
except ImportError:
|
|
283
|
+
return False
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def short_label(value: str, limit: int = 10) -> str:
|
|
288
|
+
value = png_text(re.sub(r"[`*]|\[[^\]]*\]", "", value), "关系")
|
|
289
|
+
return value if len(value) <= limit else value[:limit - 1] + "…"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def draw_png(relations: List[Dict[str, str]], font_path: str, root: Path) -> List[str]:
|
|
293
|
+
import matplotlib
|
|
294
|
+
matplotlib.use("Agg")
|
|
295
|
+
from matplotlib import font_manager
|
|
296
|
+
import matplotlib.pyplot as plt
|
|
297
|
+
|
|
298
|
+
font = font_manager.FontProperties(fname=font_path)
|
|
299
|
+
(root / CHART_DIR).mkdir(parents=True, exist_ok=True)
|
|
300
|
+
written = []
|
|
301
|
+
core = latest_by_pair(relations)
|
|
302
|
+
degree = {} # type: Dict[str, int]
|
|
303
|
+
for relation in core:
|
|
304
|
+
for name in (relation["source"], relation["target"]):
|
|
305
|
+
degree[name] = degree.get(name, 0) + 1
|
|
306
|
+
# The most connected character (usually the protagonist) sits in the middle.
|
|
307
|
+
names = sorted(degree, key=lambda name: -degree[name])
|
|
308
|
+
ring = names[1:] if len(names) > 2 else names
|
|
309
|
+
positions = {name: (math.cos(2 * math.pi * index / len(ring)), math.sin(2 * math.pi * index / len(ring)))
|
|
310
|
+
for index, name in enumerate(ring)}
|
|
311
|
+
if len(names) > 2:
|
|
312
|
+
positions[names[0]] = (0.0, 0.0)
|
|
313
|
+
size = min(14, 8 + 0.3 * len(names))
|
|
314
|
+
figure, axis = plt.subplots(figsize=(size, size))
|
|
315
|
+
axis.set_axis_off()
|
|
316
|
+
bend = 0.18 # curve both directions of a pair apart so their labels do not overlap
|
|
317
|
+
for relation in core:
|
|
318
|
+
(x1, y1), (x2, y2) = positions[relation["source"]], positions[relation["target"]]
|
|
319
|
+
axis.annotate("", xy=(x2, y2), xytext=(x1, y1),
|
|
320
|
+
arrowprops={"arrowstyle": "->", "color": "#666666", "shrinkA": 22, "shrinkB": 22,
|
|
321
|
+
"connectionstyle": "arc3,rad=%s" % bend})
|
|
322
|
+
label_x = (x1 + x2) / 2 + bend * (y2 - y1) / 2
|
|
323
|
+
label_y = (y1 + y2) / 2 - bend * (x2 - x1) / 2
|
|
324
|
+
axis.text(label_x, label_y, short_label(relation["state"] or relation["label"] or "关系"),
|
|
325
|
+
fontproperties=font, fontsize=9, ha="center", va="center", color="#333333",
|
|
326
|
+
bbox={"boxstyle": "round", "fc": "white", "ec": "none", "alpha": 0.8})
|
|
327
|
+
for name, (x, y) in positions.items():
|
|
328
|
+
axis.text(x, y, png_text(name, "未命名"), fontproperties=font, fontsize=13, ha="center", va="center",
|
|
329
|
+
bbox={"boxstyle": "round,pad=0.5", "fc": "#e8eef7", "ec": "#4a6fa5"})
|
|
330
|
+
axis.set_aspect("equal")
|
|
331
|
+
axis.set_xlim(-1.4, 1.4)
|
|
332
|
+
axis.set_ylim(-1.4, 1.4)
|
|
333
|
+
figure.savefig(str(root / CORE_PNG), dpi=150, bbox_inches="tight")
|
|
334
|
+
plt.close(figure)
|
|
335
|
+
written.append(CORE_PNG.as_posix())
|
|
336
|
+
|
|
337
|
+
changes = evolutions(relations)
|
|
338
|
+
if changes:
|
|
339
|
+
figure, axis = plt.subplots(figsize=(10, 1.2 + 0.8 * len(changes)))
|
|
340
|
+
axis.set_axis_off()
|
|
341
|
+
for row_index, ((source, target), rows) in enumerate(changes):
|
|
342
|
+
y = len(changes) - row_index
|
|
343
|
+
pair = "%s → %s" % (png_text(source, "未命名"), png_text(target, "未命名"))
|
|
344
|
+
axis.text(0, y, pair, fontproperties=font, fontsize=11, ha="left", va="center")
|
|
345
|
+
for step, row in enumerate(rows):
|
|
346
|
+
x = 3 + step * 2.2
|
|
347
|
+
if step:
|
|
348
|
+
axis.annotate("", xy=(x - 0.6, y), xytext=(x - 1.6, y),
|
|
349
|
+
arrowprops={"arrowstyle": "->", "color": "#a58a4a"})
|
|
350
|
+
axis.text(x, y, png_text(row["state"] or row["label"], "关系"), fontproperties=font,
|
|
351
|
+
fontsize=10, ha="center", va="center",
|
|
352
|
+
bbox={"boxstyle": "round", "fc": "#f3efe6", "ec": "#a58a4a"})
|
|
353
|
+
if row["trigger"]:
|
|
354
|
+
axis.text(x, y - 0.32, png_text(row["trigger"]), fontproperties=font, fontsize=8,
|
|
355
|
+
ha="center", va="center", color="#666666")
|
|
356
|
+
axis.set_xlim(-0.2, 3 + 2.2 * max(len(rows) for _, rows in changes))
|
|
357
|
+
axis.set_ylim(0.3, len(changes) + 0.7)
|
|
358
|
+
figure.savefig(str(root / EVOLUTION_PNG), dpi=150, bbox_inches="tight")
|
|
359
|
+
plt.close(figure)
|
|
360
|
+
written.append(EVOLUTION_PNG.as_posix())
|
|
361
|
+
return written
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def render(root: Path, want_png: bool) -> Dict[str, object]:
|
|
365
|
+
source = root / RELATION_FILE
|
|
366
|
+
try:
|
|
367
|
+
text = source.read_text(encoding="utf-8-sig")
|
|
368
|
+
except OSError:
|
|
369
|
+
raise ChartError("relation_file_missing", "还没有 角色/角色关系.md,先整理人物关系,再生成关系图。")
|
|
370
|
+
relations, skipped = parse_relations(text)
|
|
371
|
+
if not relations:
|
|
372
|
+
raise ChartError("relation_table_empty", "角色/角色关系.md 里没有认得出的关系表(需要「主体 → 客体」一列),关系图没有生成。")
|
|
373
|
+
# The Markdown chart is the main result: write it before trying any picture.
|
|
374
|
+
atomic_write(root / CHART_MD, render_markdown(root.name, relations, "").encode("utf-8"))
|
|
375
|
+
png_written = [] # type: List[str]
|
|
376
|
+
messages = [] # type: List[str]
|
|
377
|
+
if skipped:
|
|
378
|
+
messages.append("角色/角色关系.md 里有 %s 行没认出是谁和谁的关系,没画进关系图。" % skipped)
|
|
379
|
+
if want_png:
|
|
380
|
+
drawn = "".join(png_text(value) for relation in relations
|
|
381
|
+
for value in (relation["source"], relation["target"], relation["state"],
|
|
382
|
+
relation["label"], relation["trigger"]))
|
|
383
|
+
font_path, missing = find_cjk_font(drawn)
|
|
384
|
+
if not matplotlib_available():
|
|
385
|
+
messages.append(NO_MATPLOTLIB_MESSAGE)
|
|
386
|
+
elif not font_path:
|
|
387
|
+
messages.append(MISSING_GLYPH_MESSAGE % "".join(missing[:5]) if missing else NO_FONT_MESSAGE)
|
|
388
|
+
else:
|
|
389
|
+
try:
|
|
390
|
+
png_written = draw_png(relations, font_path, root)
|
|
391
|
+
except Exception: # noqa: BLE001 - the Markdown chart is already written
|
|
392
|
+
messages.append(PNG_FAILED_MESSAGE)
|
|
393
|
+
if png_written:
|
|
394
|
+
atomic_write(root / CHART_MD,
|
|
395
|
+
render_markdown(root.name, relations, "图片版见同目录的 PNG。").encode("utf-8"))
|
|
396
|
+
return {
|
|
397
|
+
"ok": True, "markdown": CHART_MD.as_posix(), "png": png_written,
|
|
398
|
+
"relations": len(relations), "skipped_rows": skipped,
|
|
399
|
+
"author_message": "".join(messages) or None,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def main() -> int:
|
|
404
|
+
for stream in (sys.stdout, sys.stderr):
|
|
405
|
+
if hasattr(stream, "reconfigure"):
|
|
406
|
+
stream.reconfigure(encoding="utf-8")
|
|
407
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
408
|
+
parser.add_argument("--root", required=True, type=Path, help="拆文库/{书名} 目录")
|
|
409
|
+
parser.add_argument("--png", action="store_true", help="also draw PNG pictures when a Chinese font exists")
|
|
410
|
+
args = parser.parse_args()
|
|
411
|
+
try:
|
|
412
|
+
if not args.root.is_dir():
|
|
413
|
+
raise ChartError("root_not_found", "没找到这本书的拆文目录,确认书名或路径。")
|
|
414
|
+
payload = render(args.root.resolve(), args.png)
|
|
415
|
+
except ChartError as exc:
|
|
416
|
+
payload = {"ok": False, "error": exc.code, "author_message": exc.author_message}
|
|
417
|
+
except (OSError, UnicodeError) as exc:
|
|
418
|
+
payload = {"ok": False, "error": "io_error", "detail": str(exc),
|
|
419
|
+
"author_message": "关系图文件写入失败,检查拆文目录是否可写。"}
|
|
420
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
421
|
+
return 0 if payload.get("ok") else 2
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if __name__ == "__main__":
|
|
425
|
+
raise SystemExit(main())
|
|
@@ -234,7 +234,10 @@ node scripts/jjwxc-rank-scraper.js --type 12 --list-only # 只
|
|
|
234
234
|
|
|
235
235
|
### Phase 4:输出扫榜报告
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
报告写给作者:讲市场结论和能写的方向。脚本名、命令和采集状态码不进报告;某个榜没采到,就说一句「XX 榜这次没拿到(原因),结论不含它」。
|
|
238
|
+
|
|
239
|
+
<!-- author-report -->
|
|
240
|
+
```md
|
|
238
241
|
# 长篇网文扫榜报告:{平台名称}
|
|
239
242
|
|
|
240
243
|
## 市场概况
|