feat: Add linked scene/chapter templates with planning-draft file references

- Updated Plot scene template with YAML frontmatter for metadata tracking
- Updated First Draft scene template with YAML frontmatter
- Added _template.md files in both Plot and First Draft folders
- Added create_scene.py script for automatic file creation and linking
- Planning files reference draft files via 'draft_file' field
- Draft files reference planning files via 'planning_template' field
- Word count targets tracked in planning, current count in draft
- Added comprehensive scripts/README.md with usage guide
- Supports --from-planning and --from-draft for creating missing counterparts

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
landon
2026-09-15 14:20:44 -05:00
parent d311e87836
commit b183237d59
6 changed files with 1117 additions and 12 deletions
@@ -0,0 +1,314 @@
# Scene Management Scripts
Automate the creation and linking of planning and draft files.
---
## Scripts Available
### `create_scene.py`
Creates linked planning and draft files for new scenes.
---
## Quick Start
### 1. Create a New Scene Pair
```bash
# Navigate to your project root
cd /path/to/your/project
# Create a new scene (act 1, chapter 1, scene 2)
python scripts/create_scene.py --act 1 --chapter 1 --scene 2 --title "Chapter Two: The Journey"
```
This creates:
- `05-Plot/act_1/scene_02.md` (planning template)
- `06-First Draft/Act 1/scene02.md` (draft file)
- Both files are automatically linked to each other
### 2. Create Draft from Existing Planning
```bash
# If you have an existing planning file but no draft
python scripts/create_scene.py --from-planning 05-Plot/act_1/scene_01.md
```
This creates the corresponding draft file and links it.
### 3. Create Planning from Existing Draft
```bash
# If you have an existing draft file but no planning
python scripts/create_scene.py --from-draft "06-First Draft/Act 1/scene01.md"
```
This creates the corresponding planning file and links it.
---
## Usage Examples
### Minimal (auto-generates title)
```bash
python scripts/create_scene.py --scene 3
```
Creates scene 3 in act 1, chapter 1 with title "Chapter 1: Scene 3"
### Full specification
```bash
python scripts/create_scene.py \
--act 2 \
--chapter 3 \
--scene 5 \
--title "Chapter Three: The Revelation" \
--type chapter
```
### Batch create multiple scenes
```bash
for scene in {2..10}; do
python scripts/create_scene.py --chapter 1 --scene $scene
Done
```
---
## File Structure
### Planning Files (`05-Plot/`)
```
05-Plot/
├── _template.md # Template for new planning files
├── act_1/
│ ├── scene_01.md # Planning for act 1, scene 1
│ ├── scene_02.md # Planning for act 1, scene 2
│ └── ...
├── act_2/
│ ├── scene_01.md
│ └── ...
└── act_3/
└── ...
```
Each planning file contains:
- YAML frontmatter with metadata (act, chapter, scene, word count targets, etc.)
- `draft_file` field pointing to the corresponding draft file
- Scene planning sections (setup, reveal, notes)
### Draft Files (`06-First Draft/`)
```
06-First Draft/
├── _template.md # Template for new draft files
├── Act 1/
│ ├── scene01.md # Draft for act 1, scene 1
│ ├── scene02.md # Draft for act 1, scene 2
│ └── ...
├── Act 2/
│ ├── scene01.md
│ └── ...
└── Act 3/
└── ...
```
Each draft file contains:
- YAML frontmatter with metadata
- `planning_template` field pointing to the corresponding planning file
- Word count tracking (current, session, target from planning)
- Actual scene content
---
## Linking System
The two files are linked via relative paths in their frontmatter:
**Planning file (`05-Plot/act_1/scene_01.md`):**
```yaml
---
draft_file: "../../06-First Draft/Act 1/scene01.md"
---
```
**Draft file (`06-First Draft/Act 1/scene01.md`):**
```yaml
---
planning_template: "../../05-Plot/act_1/scene_01.md"
---
```
This bidirectional linking allows:
- Navigating from planning to draft and vice versa
- Sharing metadata between files
- Tracking progress across both
---
## Metadata Reference
### Planning File Fields
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier (e.g., `scene_01_01_01`) |
| `title` | string | Scene/chapter title |
| `slug` | string | URL-friendly identifier |
| `act` | int | Act number |
| `chapter` | int | Chapter number |
| `scene` | int | Scene number |
| `type` | string | `scene` \| `chapter` \| `interlude` |
| `status` | string | `planning` \| `ready` \| `blocked` |
| `word_count_target` | int | Target word count |
| `word_count_min` | int | Minimum acceptable words |
| `word_count_max` | int | Maximum acceptable words |
| `pov` | string | Point of view character |
| `characters` | list | Characters in this scene |
| `setting` | string | Primary location |
| `date` | string | Date in story timeline |
| `time` | string | Time of day |
| `conflict` | string | Central conflict |
| `theme` | string | Thematic focus |
| `tone` | string | Emotional tone |
| `draft_file` | string | Path to linked draft file |
| `order` | int | Sort order |
### Draft File Fields
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier (e.g., `draft_scene_01_01_01`) |
| `title` | string | Scene/chapter title |
| `slug` | string | URL-friendly identifier |
| `act` | int | Act number |
| `chapter` | int | Chapter number |
| `scene` | int | Scene number |
| `type` | string | `draft` |
| `status` | string | `not_started` \| `in_progress` \| `completed` \| `needs_revision` |
| `word_count` | int | Current word count |
| `word_count_updated` | string | Last word count update date |
| `session_word_count` | int | Words written in current session |
| `last_session` | string | Date of last writing session |
| `total_sessions` | int | Number of writing sessions |
| `pov` | string | Point of view character |
| `version` | int | Draft version |
| `planning_template` | string | Path to linked planning file |
---
## Integration with Your Workflow
### With Drag-and-Drop Reordering
If your editor supports drag-and-drop chapter reordering, the `order` field in the frontmatter is available for sorting. Update this field when reordering scenes.
### With File Watchers
To automatically create corresponding files when a new file is created:
1. **Option A: Use the script manually** (recommended)
```bash
# After creating a planning file manually, run:
python scripts/create_scene.py --from-planning 05-Plot/act_1/scene_05.md
```
2. **Option B: Set up a file watcher**
Create a watcher script that triggers `create_scene.py` when new files are detected.
3. **Option C: Editor integration**
If your editor supports custom commands, add a command that calls:
```bash
python scripts/create_scene.py --from-draft "${file}"
```
---
## Tips
1. **Naming Convention**
- Planning: `scene_{number:02d}.md` (e.g., `scene_01.md`, `scene_02.md`)
- Draft: `scene{number:02d}.md` (e.g., `scene01.md`, `scene02.md`)
- Note the slight difference (underscore vs. no underscore)
2. **Folder Structure**
- Planning: `05-Plot/act_{N}/`
- Draft: `06-First Draft/Act N/`
- Note the difference in folder naming (act_1 vs. Act 1)
3. **ID Generation**
- Planning IDs: `scene_{act:02d}_{chapter:02d}_{scene:02d}`
- Draft IDs: `draft_scene_{act:02d}_{chapter:02d}_{scene:02d}`
4. **Word Count Targets**
- Set in planning file
- Displayed in draft file
- Not automatically synced (manual update or via script)
---
## Troubleshooting
### "Module not found: yaml"
```bash
pip install pyyaml
```
### File paths not working
- Ensure you're running the script from the project root
- Check that the `scripts/` directory is in your project
- Verify relative paths are correct for your folder structure
### Frontmatter not parsing
- Ensure your files start with `---` on the first line
- Ensure there's a closing `---` after the frontmatter
- YAML is sensitive to indentation (use spaces, not tabs)
---
## Advanced Usage
### Custom Metadata
Add any additional fields to the frontmatter:
```bash
python scripts/create_scene.py --scene 1 --custom_field "value"
```
### Bulk Operations
Create multiple scenes at once:
```bash
# Create scenes 1-10 for chapter 1
for i in {1..10}; do
python scripts/create_scene.py --chapter 1 --scene $i
done
```
### Sync Existing Files
If you have existing files without links, you can manually add the frontmatter:
**Planning file:**
```yaml
---
draft_file: "../../06-First Draft/Act 1/scene01.md"
---
```
**Draft file:**
```yaml
---
planning_template: "../../05-Plot/act_1/scene_01.md"
---
```
---
## Related Files
- `05-Plot/_template.md` - Planning file template
- `06-First Draft/_template.md` - Draft file template
- `03-Story-Bible/` - Story Bible for character/location/lore tracking
@@ -0,0 +1,395 @@
#!/usr/bin/env python3
"""
Scene Creation Script
Creates linked planning and draft files for a new scene.
Usage:
python create_scene.py --act 1 --chapter 1 --scene 2 --title "Chapter Two: The Journey"
Or with minimal args (auto-generates title):
python create_scene.py --act 1 --chapter 1 --scene 2
Or create from existing planning file:
python create_scene.py --from-planning 05-Plot/act_1/scene_01.md
Or create from existing draft file:
python create_scene.py --from-draft "06-First Draft/Act 1/scene01.md"
"""
import argparse
import os
import re
import sys
from pathlib import Path
from datetime import datetime
# Get the project root (parent of scripts directory)
SCRIPTS_DIR = Path(__file__).parent
PROJECT_ROOT = SCRIPTS_DIR.parent
# Template paths
PLANNING_TEMPLATE = PROJECT_ROOT / "05-Plot" / "_template.md"
DRAFT_TEMPLATE = PROJECT_ROOT / "06-First Draft" / "_template.md"
def slugify(text):
"""Convert text to URL-friendly slug."""
if not text:
return ""
# Convert to lowercase
text = text.lower()
# Remove special characters
text = re.sub(r'[^\w\s-]', '', text)
# Replace spaces with hyphens
text = re.sub(r'[\s_]+', '-', text)
# Remove leading/trailing hyphens
text = text.strip('-')
return text
def generate_title(act, chapter, scene):
"""Generate a default title based on act/chapter/scene."""
return f"Chapter {chapter}: Scene {scene}"
def parse_yaml_frontmatter(content):
"""Parse YAML frontmatter from markdown file."""
import yaml
if not content.startswith('---'):
return {}
# Find the frontmatter
lines = content.split('\n')
if lines[0].strip() == '---':
frontmatter_lines = []
for line in lines[1:]:
if line.strip() == '---':
break
frontmatter_lines.append(line)
try:
return yaml.safe_load('\n'.join(frontmatter_lines)) or {}
except yaml.YAMLError:
return {}
return {}
def update_frontmatter(content, updates):
"""Update YAML frontmatter in markdown content."""
lines = content.split('\n')
if not lines[0].strip() == '---':
# No frontmatter, add it
yaml_content = "\n".join(f"{k}: {v}" for k, v in updates.items())
return f"---\n{yaml_content}\n---\n\n" + content
# Find and replace frontmatter
in_frontmatter = False
frontmatter_start = 0
frontmatter_end = 0
for i, line in enumerate(lines):
if line.strip() == '---':
if not in_frontmatter:
frontmatter_start = i
in_frontmatter = True
else:
frontmatter_end = i
break
if frontmatter_end == 0:
return content
# Parse existing frontmatter
existing = parse_yaml_frontmatter(content)
existing.update(updates)
# Generate new frontmatter
yaml_lines = ["---"]
for key, value in existing.items():
if isinstance(value, str):
yaml_lines.append(f"{key}: \"{value}\"")
elif isinstance(value, list):
yaml_lines.append(f"{key}: {value}")
else:
yaml_lines.append(f"{key}: {value}")
yaml_lines.append("---")
# Reconstruct content
new_content = "\n".join(yaml_lines) + "\n\n" + "\n".join(lines[frontmatter_end + 1:])
return new_content
def create_planning_file(act, chapter, scene, title, slug=None, **kwargs):
"""Create a new planning file."""
if slug is None:
slug = slugify(title)
# Create path
act_dir = PROJECT_ROOT / "05-Plot" / f"act_{act}"
act_dir.mkdir(parents=True, exist_ok=True)
filename = act_dir / f"scene_{scene:02d}.md"
# Read template
with open(PLANNING_TEMPLATE, 'r') as f:
template_content = f.read()
# Generate ID
scene_id = f"scene_{act:02d}_{chapter:02d}_{scene:02d}"
# Prepare frontmatter updates
frontmatter = {
'id': scene_id,
'title': title,
'slug': slug,
'act': act,
'chapter': chapter,
'scene': scene,
'order': scene,
'created': datetime.now().strftime("%Y-%m-%d"),
'updated': datetime.now().strftime("%Y-%m-%d"),
}
# Update with any additional kwargs
frontmatter.update(kwargs)
# Update draft_file path if not provided
if 'draft_file' not in frontmatter:
draft_path = f"../../06-First Draft/Act {act}/scene{scene:02d}.md"
frontmatter['draft_file'] = draft_path
# Update content
content = update_frontmatter(template_content, frontmatter)
# Write file
with open(filename, 'w') as f:
f.write(content)
return filename
def create_draft_file(act, chapter, scene, title, slug=None, planning_path=None, **kwargs):
"""Create a new draft file."""
if slug is None:
slug = slugify(title)
# Create path
act_dir = PROJECT_ROOT / "06-First Draft" / f"Act {act}"
act_dir.mkdir(parents=True, exist_ok=True)
filename = act_dir / f"scene{scene:02d}.md"
# Read template
with open(DRAFT_TEMPLATE, 'r') as f:
template_content = f.read()
# Generate ID
scene_id = f"draft_scene_{act:02d}_{chapter:02d}_{scene:02d}"
# Prepare frontmatter updates
frontmatter = {
'id': scene_id,
'title': title,
'slug': slug,
'act': act,
'chapter': chapter,
'scene': scene,
'created': datetime.now().strftime("%Y-%m-%d"),
'updated': datetime.now().strftime("%Y-%m-%d"),
}
# Update with any additional kwargs
frontmatter.update(kwargs)
# Set planning_template path if not provided
if 'planning_template' not in frontmatter:
if planning_path:
# Use the provided planning path
relative_path = os.path.relpath(planning_path, act_dir)
frontmatter['planning_template'] = relative_path
else:
# Generate default path
planning_path = f"../../05-Plot/act_{act}/scene_{scene:02d}.md"
frontmatter['planning_template'] = planning_path
# Update content
content = update_frontmatter(template_content, frontmatter)
# Write file
with open(filename, 'w') as f:
f.write(content)
return filename
def link_files(planning_path, draft_path):
"""Ensure both files are linked to each other."""
# Update planning file with draft path
with open(planning_path, 'r') as f:
planning_content = f.read()
relative_draft = os.path.relpath(draft_path, Path(planning_path).parent)
planning_content = update_frontmatter(planning_content, {
'draft_file': relative_draft
})
with open(planning_path, 'w') as f:
f.write(planning_content)
# Update draft file with planning path
with open(draft_path, 'r') as f:
draft_content = f.read()
relative_planning = os.path.relpath(planning_path, Path(draft_path).parent)
draft_content = update_frontmatter(draft_content, {
'planning_template': relative_planning
})
with open(draft_path, 'w') as f:
f.write(draft_content)
def create_scene_pair(act, chapter, scene, title=None, **kwargs):
"""Create both planning and draft files."""
if title is None:
title = generate_title(act, chapter, scene)
slug = slugify(title)
# Create planning file
planning_path = create_planning_file(
act, chapter, scene, title, slug, **kwargs
)
# Create draft file (without planning_path initially)
draft_path = create_draft_file(
act, chapter, scene, title, slug, **kwargs
)
# Link them together
link_files(planning_path, draft_path)
print(f"✓ Created planning file: {planning_path.relative_to(PROJECT_ROOT)}")
print(f"✓ Created draft file: {draft_path.relative_to(PROJECT_ROOT)}")
print(f"✓ Linked files together")
return planning_path, draft_path
def create_from_planning(planning_path_str):
"""Create a draft file from an existing planning file."""
planning_path = Path(planning_path_str)
if not planning_path.exists():
print(f"Error: Planning file not found: {planning_path}")
sys.exit(1)
# Parse planning file
with open(planning_path, 'r') as f:
planning_content = f.read()
frontmatter = parse_yaml_frontmatter(planning_content)
if not frontmatter:
print(f"Error: No valid frontmatter in {planning_path}")
sys.exit(1)
act = frontmatter.get('act', 1)
chapter = frontmatter.get('chapter', 1)
scene = frontmatter.get('scene', 1)
title = frontmatter.get('title', generate_title(act, chapter, scene))
# Create draft file
draft_path = create_draft_file(
act, chapter, scene, title,
planning_path=planning_path
)
# Link files
link_files(planning_path, draft_path)
print(f"✓ Created draft file: {draft_path.relative_to(PROJECT_ROOT)}")
print(f"✓ Linked to planning file: {planning_path.relative_to(PROJECT_ROOT)}")
return draft_path
def create_from_draft(draft_path_str):
"""Create a planning file from an existing draft file."""
draft_path = Path(draft_path_str)
if not draft_path.exists():
print(f"Error: Draft file not found: {draft_path}")
sys.exit(1)
# Parse draft file
with open(draft_path, 'r') as f:
draft_content = f.read()
frontmatter = parse_yaml_frontmatter(draft_content)
if not frontmatter:
print(f"Error: No valid frontmatter in {draft_path}")
sys.exit(1)
act = frontmatter.get('act', 1)
chapter = frontmatter.get('chapter', 1)
scene = frontmatter.get('scene', 1)
title = frontmatter.get('title', generate_title(act, chapter, scene))
# Create planning file
planning_path = create_planning_file(
act, chapter, scene, title,
draft_file=draft_path
)
# Link files
link_files(planning_path, draft_path)
print(f"✓ Created planning file: {planning_path.relative_to(PROJECT_ROOT)}")
print(f"✓ Linked to draft file: {draft_path.relative_to(PROJECT_ROOT)}")
return planning_path
def main():
parser = argparse.ArgumentParser(
description='Create linked planning and draft files for scenes.'
)
# For creating new scene pair
parser.add_argument('--act', type=int, default=1, help='Act number')
parser.add_argument('--chapter', type=int, default=1, help='Chapter number')
parser.add_argument('--scene', type=int, required=True, help='Scene number')
parser.add_argument('--title', type=str, default=None, help='Scene title')
parser.add_argument('--type', type=str, default='scene',
choices=['scene', 'chapter', 'interlude'],
help='Scene type')
# For creating from existing files
parser.add_argument('--from-planning', type=str, default=None,
help='Create draft from existing planning file')
parser.add_argument('--from-draft', type=str, default=None,
help='Create planning from existing draft file')
args = parser.parse_args()
if args.from_planning:
create_from_planning(args.from_planning)
elif args.from_draft:
create_from_draft(args.from_draft)
else:
create_scene_pair(
act=args.act,
chapter=args.chapter,
scene=args.scene,
title=args.title,
type=args.type
)
if __name__ == '__main__':
main()