narrative-lion
Narrative Lion
Use the CLI at ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py for all API operations. Run via Bash:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py <command> [args...] [--json]
Auth
export NLK_API_KEY=nlk_xxxxxxxx
Create one at https://narrativelion.com/settings/api-keys (Pro plan required).
All API calls use Authorization: Bearer $NLK_API_KEY header.
CLI Reference
General
| Command | Description |
|---|---|
nl.py search <query> [--collection ID] |
Semantic search |
nl.py fts <query> [--collection ID] |
Full-text keyword search |
nl.py notes list [--collection ID] [--type T] [--uncategorized] [--starred] |
Browse notes |
nl.py notes get <noteId> |
Note detail |
nl.py notes create --type T --content C [--file path] [--skip-ai] |
Create note |
nl.py export <noteId> [noteId2 ...] |
Export as Markdown zip |
nl.py usage |
Credit usage |
Film Director
| Command | Description |
|---|---|
nl.py director <concept> [--type T] [--duration N] [--aspect R] [--style S] |
Generate storyboard from concept (costs 1-2 credits) |
nl.py director-persist <threadId> --storyboard <md> --instruction <text> |
Persist storyboard as filmwork note (no LLM cost) |
Filmwork
| Command | Description |
|---|---|
nl.py overview <noteId> |
Project overview: status counts + all shots |
nl.py shot <noteId> <label> |
Shot detail: preflight, assets, rolls, prompt |
nl.py preflight <noteId> <label> |
Preflight check only |
nl.py upload <shotId> <assetType> <file> [--label L] [--method M --model M --prompt P --user-note N --parent JSON] |
Upload asset (handles 3-step flow, optional provenance) |
nl.py upload-roll <shotId> <file> [--seed N --model M --prompt-version N] |
Upload roll video |
nl.py shot-update <shotId> [--status S] [--blocker JSON] [--prompts JSON] [--dialogue JSON] [--direction JSON] [--model-config JSON] [--relations JSON] [--duration N] |
Update shot status and/or fields |
nl.py score <rollId> --face N --expr N --motion N --stability N --style N |
Score a roll (auto-computes weighted total) |
nl.py verdict <rollId> <approved|rejected> |
Set roll verdict |
nl.py golden-roll <rollId> |
Set golden roll |
nl.py decision <noteId> [--shot ID] --action A --reason R --outcome O |
Log decision |
nl.py insight <noteId> --category C --tags T1,T2 --title T --detail D [--source-shots JSON] |
Log insight |
nl.py decisions <noteId> [--shot ID] [--limit N] [--offset N] |
List decisions |
nl.py insights [noteId] [--category C] [--tag T] [--limit N] [--offset N] |
List insights (cross-note if no noteId) |
nl.py provenance <assetId> |
Query how an asset was made |
nl.py lineage <assetId> [--depth N] |
Query full lineage DAG |
nl.py roll-snapshot <rollId> |
What asset versions were used to generate a roll |
nl.py set-provenance <assetId> --method M [--model M] [--prompt P] [--parent JSON ...] |
Set/update provenance after the fact |
nl.py download <assetId> <output_path> |
Download a single asset to local file |
nl.py download-shot <noteId> <label> [--dir D] [--all] |
Download golden assets for a shot (--all for every version) |
All commands support --json for raw JSON output.
Note: upload and upload-roll take the shot UUID (the id field), not the label. Get the UUID from nl.py shot <noteId> <label>.
Best Practices
Search
search is semantic — finds results even when titles don't match.
fts is exact keyword matching. Both accept --collection.
Collections
Two-level folder tree. notes list --collection ID scopes by collection.
notes list --uncategorized returns notes not in any collection.
Chat / SSE
Film Director Notes
director costs 1-2 credits per call.
Threading: omit --thread for a new conversation, pass --thread <id> to continue.
Podcast
Podcast notes store IR JSON in metadata. Content is IR, not markdown.
Create (0 credits):
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py notes create \
--type podcast --skip-ai --content '<IR JSON>'
Minimal IR structure:
{
"speakers": [
{
"id": "speaker_1",
"name": "Eva",
"role": "host",
"gender": "female",
"personality": "Professional, warm"
}
],
"segments": [
{
"id": "seg_ax",
"speakerId": "speaker_1",
"script": "[confident] Welcome to the show.",
"pauseAfterMs": 300
},
{
"id": "seg_bq",
"speakerId": "speaker_1",
"script": "Thanks for listening.",
"pauseAfterMs": 0
}
],
"globalSettings": {
"modelId": "eleven_v3",
"outputFormat": "mp3_44100_128",
"language": "en"
}
}
IR rules:
speakers[].id: usespeaker_1,speaker_2, etc.segments[].id:seg_+ 2 random lowercase letters (e.g.seg_ax,seg_bq). Must be unique.segments[].speakerId: must reference aspeakers[].id.script: optional[tag]prefix for voice direction —[whispering],[excited],[confident],[pause],[laughs]. Use...for natural pauses with eleven_v3.pauseAfterMs: required. 200–500 for normal, 500–800 for topic transitions.- Do NOT include
audio,audioHistory, oraudioUnsynced— system-managed. voiceIdis optional on speakers. If omitted, assign via Studio UI or specify from premade catalog.- For duo podcasts, add a second speaker with
"role": "cohost".
Premade voice catalog (no custom ElevenLabs key needed):
Female: Rachel (21m00Tcm4TlvDq8ikWAM) calm/analytical/warm, Sarah (EXAVITQu4vr4xnSDxMaL) warm/thoughtful, Alice (Xb7hH8MSUJpSbSDYk0k2) excited/passionate, Matilda (XrExE9yKIg1WjnnlVkGX) warm/reflective, Lily (pFZP5JQG7iQjIQuC4Bku) tense/serious, Emily (LcfcDJNUP1GQjkzn1xUU) calm/reflective.
Male: Brian (nPczCjzI2devNBz1zQrb) calm/analytical/thoughtful, Daniel (onwK4e9ZLuTAKqWW03F9) serious/thoughtful, Chris (iP95p4xoKVk53GoZ742B) curious/playful/casual, Charlie (IKne3meq5aSn9XLyUdCD) playful/casual, Bill (pqHfZKP75CvOlQylNhV4) passionate/serious, Josh (TxGEqnHWrfWFTfGW9XjX) excited/passionate.
Generate TTS (requires podcast scope, costs credits):
mutation {
generateSegmentAudio(noteId: "...", segmentId: "seg_ax") {
segmentId audioUrl durationMs
}
}
AI script editing (1 credit):
POST /api/podcast/edit with { "noteId": "...", "prompt": "Make it more conversational" }. Add "segmentId": "seg_ax" to scope to one segment. Response: { changes, summary }. Not auto-applied — call updateNote(metadata: updatedIR) to persist.
Update IR:
mutation {
updateNote(noteId: "...", metadata: "<stringified IR JSON>") { id updatedAt }
}
noteMd is auto-derived from IR on every updateNote(metadata: ...) call.
Export audio:
mutation {
exportPodcast(noteId: "...", format: "mp3", paddingMs: 200) {
url durationMs
}
}
format: "mp3" (default) merges all segments into one file with silence gaps; "zip" gives one MP3 per segment, no gaps. paddingMs (0-10000) adds extra silence after each segment (mp3 only). Download via GET /api/audio/<noteId>/export. Speed adjustment is UI-only (not available via API).
Full docs: https://narrativelion.com/docs/podcast
Filmwork Production
Creating a Project
| Path | When | Cost |
|---|---|---|
| A: Film Director | Have concept, need storyboard | 1-2 credits |
| B: Direct creation | Have formatted storyboard | 0 credits |
Path A:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py director "concept description" --type animate --duration 30 --aspect 16:9
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py director-persist <threadId> --storyboard <md> --instruction "concept" --type animate --duration 30 --aspect 16:9
Path B: notes create --type filmwork --content <storyboard_md> then create shots via GraphQL.
Labels must match **01A** (Ns) — Title. Invalid → INVALID_STORYBOARD_FORMAT.
Agent Hold
shot output marks assets/rolls with [HOLD]. If [HOLD], skip that item.
Scanning Project State
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py overview <noteId>
Gives status counts + per-shot summary (assets, rolls, best score, preflight). Drill into a specific shot:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py shot <noteId> <label>
Sync to Local Studio
Download golden assets to the local studio directory:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py download-shot <noteId> <label> --dir studio/scene/season01/ep01/final/<label>_<scene>/
Studio directory convention: studio/scene/season01/ep01/final/{label}_{scene_name}/
File naming: {label}_FINAL_start_frame.png, {label}_FINAL_end_frame.png, {label}_SFX_{name}.mp3, etc.
Before Starting Work on a Shot
Always check existing knowledge first:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py decisions <noteId> --shot <shotUUID>
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py insights <noteId>
5 seconds of reading prevents 5 minutes of repeating past mistakes.
Before Writing or Updating Prompts
Spawn a sub-agent (via the Agent tool) to review relevant insights before writing or modifying a video prompt. This avoids loading raw insight data into the main context.
Sub-agent instructions:
- Run
nl.py insights --category video --json(cross-note; or add<noteId>to scope to one project) - For image generation, use
--category imageinstead - Read through all returned insights
- Return a short bullet list of insights relevant to the current shot's content (camera movement, character action, scene type)
- If no insights are relevant, return "No relevant prompt insights found"
Use --tag to narrow results when you know the topic (e.g. --tag motion-direction, --tag start-end-pair).
The Production Loop
Prepare → Preflight → Generate → Review → Act on verdict
↑ |
└───────────────┘
Prepare
- Upload reference assets with provenance:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py upload <shotUUID> start_frame /path/to/frame.png \ --method ai_generated --model "gpt-image-2" --prompt "Winter city street..." \ --parent '{"assetId":"<prev-asset-id>","role":"base"}' \ --parent '{"externalRef":"claire_fullbody.png — LoRA tier-1","role":"reference"}' - For collection types (keyframe, sfx, ref_image), use
--label:python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py upload <shotUUID> sfx /path/to/city_ambience.mp3 --label "City ambience" - For user-uploaded assets (no generation info), use
--method user_upload --user-note:python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py upload <shotUUID> ref_image /path/to/ref.png \ --label "Character ref" --method user_upload --user-note "From LoRA training set"
Preflight
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py preflight <noteId> <label>
Three checks: start_frame ready, audio ready, active_prompt exists. Do not generate until all pass.
Generate & Upload
Generate video externally, then upload:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py upload-roll <shotUUID> /path/to/video.mp4 --seed 42 --model "kling-2.6" --prompt-version 1
Review & Score
Score across 5 weighted dimensions (max 55 points):
| Dimension | Flag | Weight |
|---|---|---|
| Face consistency | --face |
x3 |
| Expression fidelity | --expr |
x3 |
| Motion / morph | --motion |
x2 |
| Stability | --stability |
x2 |
| Style match | --style |
x1 |
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py score <rollId> --face 4 --expr 3 --motion 4 --stability 4 --style 4
Act on Verdict
>= 45 — Golden. Lock it:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py verdict <rollId> approved
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py golden-roll <rollId>
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py decision <noteId> --shot <shotUUID> --action approve_golden --reason "scored 48, strong across all dimensions" --outcome "locked as golden, moving to next shot"
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py shot-update <shotUUID> --status done
40-44 — Approve with reservation:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py verdict <rollId> approved
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py golden-roll <rollId>
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py decision <noteId> --shot <shotUUID> --action approve_with_reservation --reason "scored 42, minor hand drift at 6s" --outcome "approved, may revisit if stronger model becomes available"
30-39 — Reject & revise:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py verdict <rollId> rejected
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py decision <noteId> --shot <shotUUID> --action plan_revision --reason "face morphing in last 2s" --outcome "adding negative prompt for morphing"
Then review insights (see "Before Writing or Updating Prompts" below), fix the prompt, and re-roll. Never re-roll without changing something.
< 30 — Reject & debug. Do not re-roll. Follow playbook below.
Logging Decisions and Insights
Decisions — log after every significant action:
Actions: approve_golden, approve_with_reservation, plan_revision, switch_model, revise_prompt, block_shot, escalate_blocker, analyze_failure, strategy_change.
Insights — log when you discover reusable knowledge:
Categories (modality): image, video, voice.
Tags (required, at least 1): push-in, pull-out, pan, locked, timelapse, tracking, start-end-pair, single-frame, multi-keyframe, walking, talking, crowd, solo, reaction, insert, motion-direction, style-consistency, lip-sync, compositing, kling, wan, gpt-image, elevenlabs, scoring, review.
Example: nl.py insight <noteId> --category video --tags "motion-direction,walking" --title "..." --detail "..."
Debug Playbooks
Roll scored < 30
- Do not re-roll. Analyze first.
nl.py decisions <noteId> --shot <shotUUID>— what's been tried?nl.py insights <noteId> --tag kling(or the relevant model tag) — known issues with this model?- Check scorecard — which dimension scored lowest? That's your fix target.
nl.py overview <noteId>— any done shots? Compare golden rolls' prompts.- Log:
nl.py decision <noteId> --shot <shotUUID> --action analyze_failure --reason "..." --outcome "plan: ..." - Now fix and re-roll.
3+ rejected rolls on same shot
Stop. Do not re-roll.
- Collect all rolls' issues — find the common pattern.
nl.py decisions <noteId> --shot <shotUUID>— what's been tried so far?nl.py insights <noteId>— is this pattern documented?- Consider: switch model, rewrite prompt, change reference frames, simplify the shot.
- Log:
nl.py insight <noteId> --category video --tags "scoring,review" --title "..." --detail "Shot X required N attempts because..." - Log:
nl.py decision <noteId> --shot <shotUUID> --action strategy_change --reason "..." --outcome "new approach: ..." - Then try the new approach.
Scoring Calibration
- 55/55 = perfect, extremely rare.
- 45+ = excellent. Lock it.
- Most first-run outputs land 35-42. Normal.
- Score the model against the brief, not the brief itself.
- BG mismatch between start/end frames = reference problem. Flag, don't penalize.
- Mouth never moves for scripted line = expected (lip-sync is post). Don't penalize.
- Mouth moves when it should be still = model failure ("liveliness bias"). Penalize under expression.
Asset Provenance
Every asset should record how it was made. Provenance is always optional — it never blocks uploads.
Record inline during upload (preferred — one API call):
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py upload <shotUUID> end_frame /path/to/end.png \
--method ai_generated --model "gpt-image-2" \
--prompt "Based on start_frame, generate end frame of 5s slow push-in..." \
--parent '{"assetId":"<start-frame-id>","role":"base"}'
Record after the fact (for existing assets missing provenance):
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py set-provenance <assetId> \
--method user_upload --user-note "Downloaded from Midjourney, prompt: ..."
Query provenance and lineage:
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py provenance <assetId>
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py lineage <assetId> --depth 3
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/nl.py roll-snapshot <rollId>
Methods
ai_generated— created by an AI model (record model + prompt)user_upload— manually uploaded by user (record user_note)manual_edit— edited by hand from an existing assetderived— mechanically transformed (e.g. padded audio from dialogue)
Parent roles
base— primary input the asset was derived fromreference— visual reference that guided generationstyle— style referencemask— mask or segmentation inputcomposition— layout/composition referenceaudio— audio input (for audio-driven generation)
When to record provenance
- Always when you generate an asset with an AI model — this is the most valuable lineage data.
- Best-effort for user uploads — at minimum record
method: user_upload. - Skip for trivial derivations that don't add information.
roll-snapshotis recorded automatically when uploading a roll — no action needed.
More from rayjan0114/narrative-lion-skills
ai-video-shot-review
Debug an AI-generated video shot (Kling / Veo / Sora / Runway / etc.) by extracting strategic frames with ffmpeg, comparing against reference frames, scoring across 5 dimensions on a 55-point scale, and producing a production-readiness decision. Use when the user asks you to analyze / debug / score / review a generated video.
2nl-video-score
>
1