{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"PlanOpticon","text":"<p>AI-powered video analysis, knowledge extraction, and planning.</p> <p>PlanOpticon processes video recordings and documents into structured knowledge \u2014 transcripts, diagrams, action items, key points, and knowledge graphs. It connects to 20+ source platforms, auto-discovers available models across multiple AI providers, and produces rich multi-format output with an interactive companion REPL and planning agent.</p>"},{"location":"#features","title":"Features","text":"<ul> <li>Multi-provider AI \u2014 Automatically discovers and routes to the best available model across OpenAI, Anthropic, Google Gemini, and more</li> <li>Planning agent \u2014 Agentic analysis that adaptively adjusts depth, focus, and strategy based on content</li> <li>Companion REPL \u2014 Interactive chat interface for exploring your knowledge base conversationally</li> <li>20+ source connectors \u2014 Google Workspace, Microsoft 365, Zoom, Teams, Meet, Notion, GitHub, YouTube, Obsidian, Apple Notes, and more</li> <li>Document export \u2014 Export knowledge to Markdown, Obsidian, Notion, and exchange formats</li> <li>OAuth authentication \u2014 Built-in <code>planopticon auth</code> for Google, Dropbox, Zoom, Notion, GitHub, and Microsoft</li> <li>Smart frame extraction \u2014 Change detection for transitions + periodic capture (every 30s) for slow-evolving content like document scrolling</li> <li>People frame filtering \u2014 OpenCV face detection removes webcam/video conference frames, keeping only shared content (slides, documents, screen shares)</li> <li>Diagram extraction \u2014 Vision model-based classification detects flowcharts, architecture diagrams, charts, and whiteboards</li> <li>Knowledge graphs \u2014 Extracts entities and relationships, builds and merges knowledge graphs across videos</li> <li>Action item detection \u2014 Finds commitments, tasks, and follow-ups with assignees and deadlines</li> <li>Batch processing \u2014 Process entire folders of videos with merged knowledge graphs and cross-referencing</li> <li>Rich output \u2014 Markdown, HTML, PDF, Mermaid diagrams, SVG/PNG renderings, JSON manifests</li> <li>Cloud sources \u2014 Fetch videos from Google Drive, Dropbox, and many more cloud platforms</li> <li>Checkpoint/resume \u2014 Pipeline resumes from where it left off if interrupted \u2014 no wasted work</li> <li>Screengrab fallback \u2014 When extraction isn't perfect, captures frames with captions \u2014 something is always better than nothing</li> </ul>"},{"location":"#quick-start","title":"Quick Start","text":"<pre><code># Install from PyPI\npip install planopticon\n\n# Analyze a single video\nplanopticon analyze -i meeting.mp4 -o ./output\n\n# Ingest documents and build a knowledge graph\nplanopticon ingest ./notes/ --output ./kb --recursive\n\n# Chat with your knowledge base\nplanopticon companion --kb ./kb\n\n# Run the planning agent interactively\nplanopticon agent --kb ./kb --interactive\n\n# Query the knowledge graph\nplanopticon query stats\n\n# Export to Obsidian\nplanopticon export obsidian --input ./kb --output ./vault\n\n# Process a folder of videos\nplanopticon batch -i ./recordings -o ./output --title \"Weekly Meetings\"\n\n# See available AI models\nplanopticon list-models\n</code></pre>"},{"location":"#installation","title":"Installation","text":"PyPI (Recommended)With cloud sourcesWith everythingBinary (no Python needed) <pre><code>pip install planopticon\n</code></pre> <pre><code>pip install planopticon[cloud]\n</code></pre> <pre><code>pip install planopticon[all]\n</code></pre> <p>Download the latest binary for your platform from GitHub Releases.</p>"},{"location":"#requirements","title":"Requirements","text":"<ul> <li>Python 3.10+</li> <li>FFmpeg (for audio extraction)</li> <li>At least one API key: <code>OPENAI_API_KEY</code>, <code>ANTHROPIC_API_KEY</code>, or <code>GEMINI_API_KEY</code></li> </ul>"},{"location":"#license","title":"License","text":"<p>MIT License \u2014 Copyright (c) 2026 CONFLICT LLC. All rights reserved.</p>"},{"location":"cli-reference/","title":"CLI Reference","text":""},{"location":"cli-reference/#global-options","title":"Global options","text":"<p>These options are available on all commands.</p> Option Description <code>-v</code>, <code>--verbose</code> Enable debug-level logging <code>-C</code>, <code>--chat</code> Enable chat mode (interactive follow-up after command completes) <code>-I</code>, <code>--interactive</code> Enable interactive REPL mode <code>--version</code> Show version and exit <code>--help</code> Show help and exit"},{"location":"cli-reference/#planopticon-analyze","title":"<code>planopticon analyze</code>","text":"<p>Analyze a single video and extract structured knowledge.</p> <pre><code>planopticon analyze [OPTIONS]\n</code></pre> Option Type Default Description <code>-i</code>, <code>--input</code> PATH required Input video file path <code>-o</code>, <code>--output</code> PATH required Output directory <code>--depth</code> <code>basic\\|standard\\|comprehensive</code> <code>standard</code> Processing depth <code>--focus</code> TEXT all Comma-separated focus areas <code>--use-gpu</code> FLAG off Enable GPU acceleration <code>--sampling-rate</code> FLOAT 0.5 Frame sampling rate (fps) <code>--change-threshold</code> FLOAT 0.15 Visual change threshold <code>--periodic-capture</code> FLOAT 30.0 Capture a frame every N seconds regardless of change (0 to disable) <code>--title</code> TEXT auto Report title <code>-p</code>, <code>--provider</code> <code>auto\\|openai\\|anthropic\\|gemini\\|ollama</code> <code>auto</code> API provider <code>--vision-model</code> TEXT auto Override vision model <code>--chat-model</code> TEXT auto Override chat model"},{"location":"cli-reference/#planopticon-batch","title":"<code>planopticon batch</code>","text":"<p>Process a folder of videos in batch.</p> <pre><code>planopticon batch [OPTIONS]\n</code></pre> Option Type Default Description <code>-i</code>, <code>--input-dir</code> PATH required Directory containing videos <code>-o</code>, <code>--output</code> PATH required Output directory <code>--depth</code> <code>basic\\|standard\\|comprehensive</code> <code>standard</code> Processing depth <code>--pattern</code> TEXT <code>*.mp4,*.mkv,*.avi,*.mov,*.webm</code> File glob patterns <code>--title</code> TEXT <code>Batch Processing Results</code> Batch title <code>-p</code>, <code>--provider</code> <code>auto\\|openai\\|anthropic\\|gemini\\|ollama</code> <code>auto</code> API provider <code>--vision-model</code> TEXT auto Override vision model <code>--chat-model</code> TEXT auto Override chat model <code>--source</code> <code>local\\|gdrive\\|dropbox</code> <code>local</code> Video source <code>--folder-id</code> TEXT none Google Drive folder ID <code>--folder-path</code> TEXT none Cloud folder path <code>--recursive/--no-recursive</code> FLAG recursive Recurse into subfolders"},{"location":"cli-reference/#planopticon-list-models","title":"<code>planopticon list-models</code>","text":"<p>Discover and display available models from all configured providers.</p> <pre><code>planopticon list-models\n</code></pre> <p>No options. Queries each provider's API and displays models grouped by provider with capabilities.</p>"},{"location":"cli-reference/#planopticon-clear-cache","title":"<code>planopticon clear-cache</code>","text":"<p>Clear API response cache.</p> <pre><code>planopticon clear-cache [OPTIONS]\n</code></pre> Option Type Default Description <code>--cache-dir</code> PATH <code>$CACHE_DIR</code> Path to cache directory <code>--older-than</code> INT all Clear entries older than N seconds <code>--all</code> FLAG off Clear all cache entries"},{"location":"cli-reference/#planopticon-agent-analyze","title":"<code>planopticon agent-analyze</code>","text":"<p>Agentic video analysis \u2014 adaptive, intelligent processing that adjusts depth and focus based on content.</p> <pre><code>planopticon agent-analyze [OPTIONS]\n</code></pre> Option Type Default Description <code>-i</code>, <code>--input</code> PATH required Input video file path <code>-o</code>, <code>--output</code> PATH required Output directory <code>--depth</code> <code>basic\\|standard\\|comprehensive</code> <code>standard</code> Initial processing depth (agent may adapt) <code>--title</code> TEXT auto Report title <code>-p</code>, <code>--provider</code> <code>auto\\|openai\\|anthropic\\|gemini\\|ollama</code> <code>auto</code> API provider <code>--vision-model</code> TEXT auto Override vision model <code>--chat-model</code> TEXT auto Override chat model"},{"location":"cli-reference/#planopticon-companion","title":"<code>planopticon companion</code>","text":"<p>Interactive knowledge base companion. Opens a REPL for conversational exploration of your knowledge base.</p> <pre><code>planopticon companion [OPTIONS]\n</code></pre> Option Type Default Description <code>--kb</code> PATH auto-detect Path to knowledge base directory <code>-p</code>, <code>--provider</code> TEXT <code>auto</code> AI provider <code>--chat-model</code> TEXT auto Override chat model <p>Examples:</p> <pre><code># Start companion with auto-detected knowledge base\nplanopticon companion\n\n# Point to a specific knowledge base\nplanopticon companion --kb ./my-kb\n\n# Use a specific provider\nplanopticon companion --kb ./kb --provider anthropic --chat-model claude-sonnet-4-20250514\n</code></pre>"},{"location":"cli-reference/#planopticon-agent","title":"<code>planopticon agent</code>","text":"<p>Planning agent with adaptive analysis. Runs an agentic loop that reasons about your knowledge base, plans actions, and executes them.</p> <pre><code>planopticon agent [OPTIONS]\n</code></pre> Option Type Default Description <code>--kb</code> PATH auto-detect Path to knowledge base directory <code>-I</code>, <code>--interactive</code> FLAG off Interactive mode (ask before each action) <code>--export</code> PATH none Export agent results to a file <code>-p</code>, <code>--provider</code> TEXT <code>auto</code> AI provider <code>--chat-model</code> TEXT auto Override chat model <p>Examples:</p> <pre><code># Run the agent interactively\nplanopticon agent --kb ./kb --interactive\n\n# Run agent and export results\nplanopticon agent --kb ./kb --export ./plan.md\n\n# Use a specific model\nplanopticon agent --kb ./kb --provider openai --chat-model gpt-4o\n</code></pre>"},{"location":"cli-reference/#planopticon-query","title":"<code>planopticon query</code>","text":"<p>Query the knowledge graph directly or with natural language.</p> <pre><code>planopticon query [OPTIONS] [QUERY]\n</code></pre> Option Type Default Description <code>--db-path</code> PATH auto-detect Path to knowledge graph database <code>--mode</code> <code>direct\\|agentic</code> auto Query mode (direct for structured, agentic for natural language) <code>--format</code> <code>text\\|json\\|mermaid</code> <code>text</code> Output format <code>-I</code>, <code>--interactive</code> FLAG off Interactive REPL mode <p>Examples:</p> <pre><code># Show graph stats\nplanopticon query stats\n\n# List entities by type\nplanopticon query \"entities --type technology\"\nplanopticon query \"entities --type person\"\n\n# Find neighbors of an entity\nplanopticon query \"neighbors Alice\"\n\n# List relationships\nplanopticon query \"relationships --source Alice\"\n\n# Natural language query (requires API key)\nplanopticon query \"What technologies were discussed?\"\n\n# Output as Mermaid diagram\nplanopticon query --format mermaid \"neighbors ProjectX\"\n\n# Output as JSON\nplanopticon query --format json stats\n\n# Interactive REPL\nplanopticon query -I\n</code></pre>"},{"location":"cli-reference/#planopticon-ingest","title":"<code>planopticon ingest</code>","text":"<p>Ingest documents and files into a knowledge graph.</p> <pre><code>planopticon ingest [OPTIONS] INPUT\n</code></pre> Option Type Default Description <code>--output</code> PATH <code>./knowledge-base</code> Output directory for the knowledge base <code>--db-path</code> PATH auto Path to existing knowledge graph database to merge into <code>--recursive</code> FLAG off Recursively process directories <code>-p</code>, <code>--provider</code> TEXT <code>auto</code> AI provider <p>Examples:</p> <pre><code># Ingest a single file\nplanopticon ingest ./meeting-notes.md --output ./kb\n\n# Ingest a directory recursively\nplanopticon ingest ./docs/ --output ./kb --recursive\n\n# Merge into an existing knowledge graph\nplanopticon ingest ./new-notes/ --db-path ./kb/knowledge_graph.db --recursive\n</code></pre>"},{"location":"cli-reference/#planopticon-auth","title":"<code>planopticon auth</code>","text":"<p>Authenticate with cloud services via OAuth or API keys.</p> <pre><code>planopticon auth SERVICE [OPTIONS]\n</code></pre> Argument Values Description <code>SERVICE</code> <code>google\\|dropbox\\|zoom\\|notion\\|github\\|microsoft</code> Cloud service to authenticate with Option Type Default Description <code>--logout</code> FLAG off Remove stored credentials for the service <p>Examples:</p> <pre><code># Authenticate with Google (Drive, Meet, YouTube, etc.)\nplanopticon auth google\n\n# Authenticate with Dropbox\nplanopticon auth dropbox\n\n# Authenticate with Zoom (for recording access)\nplanopticon auth zoom\n\n# Authenticate with Notion\nplanopticon auth notion\n\n# Authenticate with GitHub\nplanopticon auth github\n\n# Authenticate with Microsoft 365 (OneDrive, Teams, etc.)\nplanopticon auth microsoft\n\n# Log out of a service\nplanopticon auth google --logout\n</code></pre>"},{"location":"cli-reference/#planopticon-gws","title":"<code>planopticon gws</code>","text":"<p>Google Workspace commands. List, fetch, and ingest content from Google Workspace (Drive, Docs, Sheets, Slides, Meet).</p>"},{"location":"cli-reference/#planopticon-gws-list","title":"<code>planopticon gws list</code>","text":"<p>List available files and recordings from Google Workspace.</p> <pre><code>planopticon gws list [OPTIONS]\n</code></pre> Option Type Default Description <code>--type</code> <code>drive\\|docs\\|sheets\\|slides\\|meet</code> all Filter by content type <code>--folder-id</code> TEXT none Google Drive folder ID <code>--limit</code> INT 50 Maximum results to return"},{"location":"cli-reference/#planopticon-gws-fetch","title":"<code>planopticon gws fetch</code>","text":"<p>Download content from Google Workspace.</p> <pre><code>planopticon gws fetch [OPTIONS] RESOURCE_ID\n</code></pre> Option Type Default Description <code>--output</code> PATH <code>./downloads</code> Output directory <code>--format</code> TEXT auto Export format (pdf, docx, etc.)"},{"location":"cli-reference/#planopticon-gws-ingest","title":"<code>planopticon gws ingest</code>","text":"<p>Ingest Google Workspace content directly into a knowledge graph.</p> <pre><code>planopticon gws ingest [OPTIONS]\n</code></pre> Option Type Default Description <code>--folder-id</code> TEXT none Google Drive folder ID <code>--output</code> PATH <code>./knowledge-base</code> Knowledge base output directory <code>--recursive</code> FLAG off Recurse into subfolders <p>Examples:</p> <pre><code># List all Google Workspace files\nplanopticon gws list\n\n# List only Google Docs\nplanopticon gws list --type docs\n\n# Fetch a specific file\nplanopticon gws fetch abc123def --output ./downloads\n\n# Ingest an entire Drive folder into a knowledge base\nplanopticon gws ingest --folder-id abc123 --output ./kb --recursive\n</code></pre>"},{"location":"cli-reference/#planopticon-m365","title":"<code>planopticon m365</code>","text":"<p>Microsoft 365 commands. List, fetch, and ingest content from Microsoft 365 (OneDrive, SharePoint, Teams, Outlook).</p>"},{"location":"cli-reference/#planopticon-m365-list","title":"<code>planopticon m365 list</code>","text":"<p>List available files and recordings from Microsoft 365.</p> <pre><code>planopticon m365 list [OPTIONS]\n</code></pre> Option Type Default Description <code>--type</code> <code>onedrive\\|sharepoint\\|teams\\|outlook</code> all Filter by content type <code>--site</code> TEXT none SharePoint site name <code>--limit</code> INT 50 Maximum results to return"},{"location":"cli-reference/#planopticon-m365-fetch","title":"<code>planopticon m365 fetch</code>","text":"<p>Download content from Microsoft 365.</p> <pre><code>planopticon m365 fetch [OPTIONS] RESOURCE_ID\n</code></pre> Option Type Default Description <code>--output</code> PATH <code>./downloads</code> Output directory"},{"location":"cli-reference/#planopticon-m365-ingest","title":"<code>planopticon m365 ingest</code>","text":"<p>Ingest Microsoft 365 content directly into a knowledge graph.</p> <pre><code>planopticon m365 ingest [OPTIONS]\n</code></pre> Option Type Default Description <code>--site</code> TEXT none SharePoint site name <code>--path</code> TEXT <code>/</code> Folder path in OneDrive/SharePoint <code>--output</code> PATH <code>./knowledge-base</code> Knowledge base output directory <code>--recursive</code> FLAG off Recurse into subfolders <p>Examples:</p> <pre><code># List all Microsoft 365 content\nplanopticon m365 list\n\n# List only Teams recordings\nplanopticon m365 list --type teams\n\n# Fetch a specific file\nplanopticon m365 fetch item-id-123 --output ./downloads\n\n# Ingest SharePoint content\nplanopticon m365 ingest --site \"Engineering\" --path \"/Shared Documents\" --output ./kb --recursive\n</code></pre>"},{"location":"cli-reference/#planopticon-recordings","title":"<code>planopticon recordings</code>","text":"<p>List meeting recordings from video conferencing platforms.</p>"},{"location":"cli-reference/#planopticon-recordings-zoom-list","title":"<code>planopticon recordings zoom-list</code>","text":"<p>List Zoom cloud recordings.</p> <pre><code>planopticon recordings zoom-list [OPTIONS]\n</code></pre> Option Type Default Description <code>--from</code> DATE 30 days ago Start date (YYYY-MM-DD) <code>--to</code> DATE today End date (YYYY-MM-DD) <code>--limit</code> INT 50 Maximum results"},{"location":"cli-reference/#planopticon-recordings-teams-list","title":"<code>planopticon recordings teams-list</code>","text":"<p>List Microsoft Teams meeting recordings.</p> <pre><code>planopticon recordings teams-list [OPTIONS]\n</code></pre> Option Type Default Description <code>--from</code> DATE 30 days ago Start date (YYYY-MM-DD) <code>--to</code> DATE today End date (YYYY-MM-DD) <code>--limit</code> INT 50 Maximum results"},{"location":"cli-reference/#planopticon-recordings-meet-list","title":"<code>planopticon recordings meet-list</code>","text":"<p>List Google Meet recordings.</p> <pre><code>planopticon recordings meet-list [OPTIONS]\n</code></pre> Option Type Default Description <code>--from</code> DATE 30 days ago Start date (YYYY-MM-DD) <code>--to</code> DATE today End date (YYYY-MM-DD) <code>--limit</code> INT 50 Maximum results <p>Examples:</p> <pre><code># List recent Zoom recordings\nplanopticon recordings zoom-list\n\n# List Teams recordings from a specific date range\nplanopticon recordings teams-list --from 2026-01-01 --to 2026-02-01\n\n# List Google Meet recordings\nplanopticon recordings meet-list --limit 10\n</code></pre>"},{"location":"cli-reference/#planopticon-export","title":"<code>planopticon export</code>","text":"<p>Export knowledge base content to various formats.</p>"},{"location":"cli-reference/#planopticon-export-markdown","title":"<code>planopticon export markdown</code>","text":"<p>Export knowledge base as Markdown files.</p> <pre><code>planopticon export markdown [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH auto-detect Knowledge base path <code>--output</code> PATH <code>./export</code> Output directory"},{"location":"cli-reference/#planopticon-export-obsidian","title":"<code>planopticon export obsidian</code>","text":"<p>Export knowledge base as an Obsidian vault with wikilinks and graph metadata.</p> <pre><code>planopticon export obsidian [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH auto-detect Knowledge base path <code>--output</code> PATH <code>./obsidian-vault</code> Output vault directory"},{"location":"cli-reference/#planopticon-export-notion","title":"<code>planopticon export notion</code>","text":"<p>Export knowledge base to Notion.</p> <pre><code>planopticon export notion [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH auto-detect Knowledge base path <code>--parent-page</code> TEXT none Notion parent page ID"},{"location":"cli-reference/#planopticon-export-exchange","title":"<code>planopticon export exchange</code>","text":"<p>Export knowledge base as PlanOpticon Exchange Format (JSON).</p> <pre><code>planopticon export exchange [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH auto-detect Knowledge base path <code>--output</code> PATH <code>./exchange.json</code> Output file path <p>Examples:</p> <pre><code># Export to Markdown\nplanopticon export markdown --input ./kb --output ./docs\n\n# Export to Obsidian vault\nplanopticon export obsidian --input ./kb --output ~/Obsidian/PlanOpticon\n\n# Export to Notion\nplanopticon export notion --input ./kb --parent-page abc123\n\n# Export as exchange format for interoperability\nplanopticon export exchange --input ./kb --output ./export.json\n</code></pre>"},{"location":"cli-reference/#planopticon-wiki","title":"<code>planopticon wiki</code>","text":"<p>Generate and publish wiki documentation from your knowledge base.</p>"},{"location":"cli-reference/#planopticon-wiki-generate","title":"<code>planopticon wiki generate</code>","text":"<p>Generate a static wiki site from the knowledge base.</p> <pre><code>planopticon wiki generate [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH auto-detect Knowledge base path <code>--output</code> PATH <code>./wiki</code> Output directory"},{"location":"cli-reference/#planopticon-wiki-push","title":"<code>planopticon wiki push</code>","text":"<p>Push a generated wiki to a remote target (e.g., GitHub Wiki, Confluence).</p> <pre><code>planopticon wiki push [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH <code>./wiki</code> Wiki directory to push <code>--target</code> TEXT required Push target (e.g., <code>github://org/repo</code>, <code>confluence://space</code>) <p>Examples:</p> <pre><code># Generate a wiki from the knowledge base\nplanopticon wiki generate --input ./kb --output ./wiki\n\n# Push wiki to GitHub\nplanopticon wiki push --input ./wiki --target \"github://ConflictHQ/project-wiki\"\n</code></pre>"},{"location":"cli-reference/#planopticon-kg","title":"<code>planopticon kg</code>","text":"<p>Knowledge graph management commands.</p>"},{"location":"cli-reference/#planopticon-kg-convert","title":"<code>planopticon kg convert</code>","text":"<p>Convert a knowledge graph between formats.</p> <pre><code>planopticon kg convert [OPTIONS]\n</code></pre> Option Type Default Description <code>--input</code> PATH required Input knowledge graph file <code>--output</code> PATH required Output file path <code>--format</code> <code>json\\|db\\|graphml\\|csv</code> auto (from extension) Target format"},{"location":"cli-reference/#planopticon-kg-sync","title":"<code>planopticon kg sync</code>","text":"<p>Synchronize two knowledge graphs (merge new data).</p> <pre><code>planopticon kg sync [OPTIONS]\n</code></pre> Option Type Default Description <code>--source</code> PATH required Source knowledge graph <code>--target</code> PATH required Target knowledge graph to merge into"},{"location":"cli-reference/#planopticon-kg-inspect","title":"<code>planopticon kg inspect</code>","text":"<p>Inspect a knowledge graph and display statistics.</p> <pre><code>planopticon kg inspect [OPTIONS] [PATH]\n</code></pre> Option Type Default Description <code>PATH</code> PATH auto-detect Knowledge graph file"},{"location":"cli-reference/#planopticon-kg-classify","title":"<code>planopticon kg classify</code>","text":"<p>Classify and tag entities in a knowledge graph.</p> <pre><code>planopticon kg classify [OPTIONS]\n</code></pre> Option Type Default Description <code>--db-path</code> PATH auto-detect Knowledge graph database <code>-p</code>, <code>--provider</code> TEXT <code>auto</code> AI provider for classification"},{"location":"cli-reference/#planopticon-kg-from-exchange","title":"<code>planopticon kg from-exchange</code>","text":"<p>Import a knowledge graph from PlanOpticon Exchange Format.</p> <pre><code>planopticon kg from-exchange [OPTIONS] INPUT\n</code></pre> Option Type Default Description <code>INPUT</code> PATH required Exchange format JSON file <code>--output</code> PATH <code>./knowledge-base</code> Output knowledge base directory <p>Examples:</p> <pre><code># Convert JSON knowledge graph to FalkorDB format\nplanopticon kg convert --input ./kg.json --output ./kg.db\n\n# Merge two knowledge graphs\nplanopticon kg sync --source ./new-kg.db --target ./main-kg.db\n\n# Inspect a knowledge graph\nplanopticon kg inspect ./knowledge_graph.db\n\n# Classify entities with AI\nplanopticon kg classify --db-path ./kg.db --provider anthropic\n\n# Import from exchange format\nplanopticon kg from-exchange ./export.json --output ./kb\n</code></pre>"},{"location":"contributing/","title":"Contributing","text":""},{"location":"contributing/#development-setup","title":"Development setup","text":"<pre><code>git clone https://github.com/ConflictHQ/PlanOpticon.git\ncd PlanOpticon\npython -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev]\"\n</code></pre>"},{"location":"contributing/#running-tests","title":"Running tests","text":"<p>PlanOpticon has 822+ tests covering providers, pipeline stages, document processors, knowledge graph operations, exporters, skills, and CLI commands.</p> <pre><code># Run all tests\npytest tests/ -v\n\n# Run with coverage\npytest tests/ --cov=video_processor --cov-report=html\n\n# Run a specific test file\npytest tests/test_models.py -v\n\n# Run tests matching a keyword\npytest tests/ -k \"test_knowledge_graph\" -v\n\n# Run only fast tests (skip slow integration tests)\npytest tests/ -m \"not slow\" -v\n</code></pre>"},{"location":"contributing/#test-conventions","title":"Test conventions","text":"<ul> <li>All tests live in the <code>tests/</code> directory, mirroring the <code>video_processor/</code> package structure</li> <li>Test files are named <code>test_<module>.py</code></li> <li>Use <code>pytest</code> as the test runner -- do not use <code>unittest.TestCase</code> unless necessary for specific setup/teardown patterns</li> <li>Mock external API calls. Never make real API calls in tests. Use <code>unittest.mock.patch</code> or <code>pytest-mock</code> fixtures to mock provider responses.</li> <li>Use <code>tmp_path</code> (pytest fixture) for any tests that write files to disk</li> <li>Fixtures shared across test files go in <code>conftest.py</code></li> <li>For testing CLI commands, use <code>click.testing.CliRunner</code></li> <li>For testing provider implementations, mock at the HTTP client level (e.g., patch <code>requests.post</code> or the provider's SDK client)</li> </ul>"},{"location":"contributing/#mocking-patterns","title":"Mocking patterns","text":"<pre><code># Mocking a provider's chat method\nfrom unittest.mock import MagicMock, patch\n\ndef test_key_point_extraction():\n pm = MagicMock()\n pm.chat.return_value = '[\"Point 1\", \"Point 2\"]'\n result = extract_key_points(pm, \"transcript text\")\n assert len(result) == 2\n\n# Mocking an external API at the HTTP level\n@patch(\"requests.post\")\ndef test_provider_chat(mock_post):\n mock_post.return_value.json.return_value = {\n \"choices\": [{\"message\": {\"content\": \"response\"}}]\n }\n provider = OpenAIProvider(api_key=\"test\")\n result = provider.chat([{\"role\": \"user\", \"content\": \"hello\"}])\n assert result == \"response\"\n</code></pre>"},{"location":"contributing/#code-style","title":"Code style","text":"<p>We use:</p> <ul> <li>Ruff for both linting and formatting (100 char line length)</li> <li>mypy for type checking</li> </ul> <p>Ruff handles all linting (error, warning, pyflakes, and import sorting rules) and formatting in a single tool. There is no need to run Black or isort separately.</p> <pre><code># Lint\nruff check video_processor/\n\n# Format\nruff format video_processor/\n\n# Auto-fix lint issues\nruff check video_processor/ --fix\n\n# Type check\nmypy video_processor/ --ignore-missing-imports\n</code></pre>"},{"location":"contributing/#ruff-configuration","title":"Ruff configuration","text":"<p>The project's <code>pyproject.toml</code> configures ruff as follows:</p> <pre><code>[tool.ruff]\nline-length = 100\ntarget-version = \"py310\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"W\", \"I\"]\n</code></pre> <p>The <code>I</code> rule set covers import sorting (equivalent to isort), so imports are automatically organized by ruff.</p>"},{"location":"contributing/#project-structure","title":"Project structure","text":"<pre><code>PlanOpticon/\n\u251c\u2500\u2500 video_processor/\n\u2502 \u251c\u2500\u2500 cli/ # Click CLI commands\n\u2502 \u2502 \u2514\u2500\u2500 commands.py\n\u2502 \u251c\u2500\u2500 providers/ # LLM/API provider implementations\n\u2502 \u2502 \u251c\u2500\u2500 base.py # BaseProvider, ProviderRegistry\n\u2502 \u2502 \u251c\u2500\u2500 manager.py # ProviderManager\n\u2502 \u2502 \u251c\u2500\u2500 discovery.py # Auto-discovery of available providers\n\u2502 \u2502 \u251c\u2500\u2500 openai_provider.py\n\u2502 \u2502 \u251c\u2500\u2500 anthropic_provider.py\n\u2502 \u2502 \u251c\u2500\u2500 gemini_provider.py\n\u2502 \u2502 \u2514\u2500\u2500 ... # 15+ provider implementations\n\u2502 \u251c\u2500\u2500 sources/ # Cloud and web source connectors\n\u2502 \u2502 \u251c\u2500\u2500 base.py # BaseSource, SourceFile\n\u2502 \u2502 \u251c\u2500\u2500 google_drive.py\n\u2502 \u2502 \u251c\u2500\u2500 zoom_source.py\n\u2502 \u2502 \u2514\u2500\u2500 ... # 20+ source implementations\n\u2502 \u251c\u2500\u2500 processors/ # Document processors\n\u2502 \u2502 \u251c\u2500\u2500 base.py # DocumentProcessor, registry\n\u2502 \u2502 \u251c\u2500\u2500 ingest.py # File/directory ingestion\n\u2502 \u2502 \u251c\u2500\u2500 markdown_processor.py\n\u2502 \u2502 \u251c\u2500\u2500 pdf_processor.py\n\u2502 \u2502 \u2514\u2500\u2500 __init__.py # Auto-registration of built-in processors\n\u2502 \u251c\u2500\u2500 integrators/ # Knowledge graph and analysis\n\u2502 \u2502 \u251c\u2500\u2500 knowledge_graph.py # KnowledgeGraph class\n\u2502 \u2502 \u251c\u2500\u2500 graph_store.py # SQLite graph storage\n\u2502 \u2502 \u251c\u2500\u2500 graph_query.py # GraphQueryEngine\n\u2502 \u2502 \u251c\u2500\u2500 graph_discovery.py # Auto-find knowledge_graph.db\n\u2502 \u2502 \u2514\u2500\u2500 taxonomy.py # Planning taxonomy classifier\n\u2502 \u251c\u2500\u2500 agent/ # Planning agent\n\u2502 \u2502 \u251c\u2500\u2500 orchestrator.py # Agent orchestration\n\u2502 \u2502 \u2514\u2500\u2500 skills/ # Skill implementations\n\u2502 \u2502 \u251c\u2500\u2500 base.py # Skill ABC, registry, Artifact\n\u2502 \u2502 \u251c\u2500\u2500 project_plan.py\n\u2502 \u2502 \u251c\u2500\u2500 prd.py\n\u2502 \u2502 \u251c\u2500\u2500 roadmap.py\n\u2502 \u2502 \u251c\u2500\u2500 task_breakdown.py\n\u2502 \u2502 \u251c\u2500\u2500 doc_generator.py\n\u2502 \u2502 \u251c\u2500\u2500 wiki_generator.py\n\u2502 \u2502 \u251c\u2500\u2500 notes_export.py\n\u2502 \u2502 \u251c\u2500\u2500 artifact_export.py\n\u2502 \u2502 \u251c\u2500\u2500 github_integration.py\n\u2502 \u2502 \u251c\u2500\u2500 requirements_chat.py\n\u2502 \u2502 \u251c\u2500\u2500 cli_adapter.py\n\u2502 \u2502 \u2514\u2500\u2500 __init__.py # Auto-registration of skills\n\u2502 \u251c\u2500\u2500 exporters/ # Output format exporters\n\u2502 \u2502 \u251c\u2500\u2500 __init__.py\n\u2502 \u2502 \u2514\u2500\u2500 markdown.py # Template-based markdown generation\n\u2502 \u251c\u2500\u2500 utils/ # Shared utilities\n\u2502 \u2502 \u251c\u2500\u2500 export.py # Multi-format export orchestration\n\u2502 \u2502 \u251c\u2500\u2500 rendering.py # Mermaid/chart rendering\n\u2502 \u2502 \u251c\u2500\u2500 prompt_templates.py\n\u2502 \u2502 \u251c\u2500\u2500 callbacks.py # Progress callback helpers\n\u2502 \u2502 \u2514\u2500\u2500 ...\n\u2502 \u251c\u2500\u2500 exchange.py # PlanOpticonExchange format\n\u2502 \u251c\u2500\u2500 pipeline.py # Main video processing pipeline\n\u2502 \u251c\u2500\u2500 models.py # Pydantic data models\n\u2502 \u2514\u2500\u2500 output_structure.py # Output directory helpers\n\u251c\u2500\u2500 tests/ # 822+ tests\n\u251c\u2500\u2500 knowledge-base/ # Local-first graph tools\n\u2502 \u251c\u2500\u2500 viewer.html # Self-contained D3.js graph viewer\n\u2502 \u2514\u2500\u2500 query.py # Python query script (NetworkX)\n\u251c\u2500\u2500 docs/ # MkDocs documentation\n\u2514\u2500\u2500 pyproject.toml # Project configuration\n</code></pre> <p>See Architecture Overview for a more detailed breakdown of module responsibilities.</p>"},{"location":"contributing/#adding-a-new-provider","title":"Adding a new provider","text":"<p>Providers self-register via <code>ProviderRegistry.register()</code> at module level. When the provider module is imported, it registers itself automatically.</p> <ol> <li>Create <code>video_processor/providers/your_provider.py</code></li> <li>Extend <code>BaseProvider</code> from <code>video_processor/providers/base.py</code></li> <li>Implement the four required methods: <code>chat()</code>, <code>analyze_image()</code>, <code>transcribe_audio()</code>, <code>list_models()</code></li> <li>Call <code>ProviderRegistry.register()</code> at module level</li> <li>Add the import to <code>video_processor/providers/manager.py</code> in the lazy-import block</li> <li>Add tests in <code>tests/test_providers.py</code></li> </ol>"},{"location":"contributing/#example-provider-skeleton","title":"Example provider skeleton","text":"<pre><code>\"\"\"Your provider implementation.\"\"\"\n\nfrom video_processor.providers.base import BaseProvider, ModelInfo, ProviderRegistry\n\n\nclass YourProvider(BaseProvider):\n provider_name = \"yourprovider\"\n\n def __init__(self, api_key: str | None = None):\n import os\n self.api_key = api_key or os.environ.get(\"YOUR_API_KEY\", \"\")\n\n def chat(self, messages, max_tokens=4096, temperature=0.7, model=None):\n # Implement chat completion\n ...\n\n def analyze_image(self, image_bytes, prompt, max_tokens=4096, model=None):\n # Implement image analysis\n ...\n\n def transcribe_audio(self, audio_path, language=None, model=None):\n # Implement audio transcription (or raise NotImplementedError)\n ...\n\n def list_models(self):\n return [ModelInfo(id=\"your-model\", provider=\"yourprovider\", capabilities=[\"chat\"])]\n\n\n# Self-registration at import time\nProviderRegistry.register(\n \"yourprovider\",\n YourProvider,\n env_var=\"YOUR_API_KEY\",\n model_prefixes=[\"your-\"],\n default_models={\"chat\": \"your-model\"},\n)\n</code></pre>"},{"location":"contributing/#openai-compatible-providers","title":"OpenAI-compatible providers","text":"<p>For providers that use the OpenAI API format, extend <code>OpenAICompatibleProvider</code> instead of <code>BaseProvider</code>. This provides default implementations of <code>chat()</code>, <code>analyze_image()</code>, and <code>list_models()</code> -- you only need to configure the base URL and model mappings.</p> <pre><code>from video_processor.providers.base import OpenAICompatibleProvider, ProviderRegistry\n\nclass YourProvider(OpenAICompatibleProvider):\n provider_name = \"yourprovider\"\n base_url = \"https://api.yourprovider.com/v1\"\n env_var = \"YOUR_API_KEY\"\n\nProviderRegistry.register(\"yourprovider\", YourProvider, env_var=\"YOUR_API_KEY\")\n</code></pre>"},{"location":"contributing/#adding-a-new-cloud-source","title":"Adding a new cloud source","text":"<p>Source connectors implement the <code>BaseSource</code> ABC from <code>video_processor/sources/base.py</code>. Authentication is handled per-source, typically via environment variables.</p> <ol> <li>Create <code>video_processor/sources/your_source.py</code></li> <li>Extend <code>BaseSource</code></li> <li>Implement <code>authenticate()</code>, <code>list_videos()</code>, and <code>download()</code></li> <li>Add the class to the lazy-import map in <code>video_processor/sources/__init__.py</code></li> <li>Add CLI commands in <code>video_processor/cli/commands.py</code> if needed</li> <li>Add tests and documentation</li> </ol>"},{"location":"contributing/#example-source-skeleton","title":"Example source skeleton","text":"<pre><code>\"\"\"Your source integration.\"\"\"\n\nimport os\nimport logging\nfrom pathlib import Path\nfrom typing import List, Optional\n\nfrom video_processor.sources.base import BaseSource, SourceFile\n\nlogger = logging.getLogger(__name__)\n\n\nclass YourSource(BaseSource):\n def __init__(self, api_key: Optional[str] = None):\n self.api_key = api_key or os.environ.get(\"YOUR_SOURCE_KEY\", \"\")\n\n def authenticate(self) -> bool:\n \"\"\"Validate credentials. Return True on success.\"\"\"\n if not self.api_key:\n logger.error(\"API key not set. Set YOUR_SOURCE_KEY env var.\")\n return False\n # Make a test API call to verify credentials\n ...\n return True\n\n def list_videos(\n self,\n folder_id: Optional[str] = None,\n folder_path: Optional[str] = None,\n patterns: Optional[List[str]] = None,\n ) -> List[SourceFile]:\n \"\"\"List available video files.\"\"\"\n ...\n\n def download(self, file: SourceFile, destination: Path) -> Path:\n \"\"\"Download a single file. Return the local path.\"\"\"\n destination.parent.mkdir(parents=True, exist_ok=True)\n # Download file content to destination\n ...\n return destination\n</code></pre>"},{"location":"contributing/#registering-in-__init__py","title":"Registering in <code>__init__.py</code>","text":"<p>Add your source to the <code>__all__</code> list and the <code>_lazy_map</code> dictionary in <code>video_processor/sources/__init__.py</code>:</p> <pre><code>__all__ = [\n ...\n \"YourSource\",\n]\n\n_lazy_map = {\n ...\n \"YourSource\": \"video_processor.sources.your_source\",\n}\n</code></pre>"},{"location":"contributing/#adding-a-new-skill","title":"Adding a new skill","text":"<p>Agent skills extend the <code>Skill</code> ABC from <code>video_processor/agent/skills/base.py</code> and self-register via <code>register_skill()</code>.</p> <ol> <li>Create <code>video_processor/agent/skills/your_skill.py</code></li> <li>Extend <code>Skill</code> and set <code>name</code> and <code>description</code> class attributes</li> <li>Implement <code>execute()</code> to return an <code>Artifact</code></li> <li>Optionally override <code>can_execute()</code> for custom precondition checks</li> <li>Call <code>register_skill()</code> at module level</li> <li>Add the import to <code>video_processor/agent/skills/__init__.py</code></li> <li>Add tests</li> </ol>"},{"location":"contributing/#example-skill-skeleton","title":"Example skill skeleton","text":"<pre><code>\"\"\"Your custom skill.\"\"\"\n\nfrom video_processor.agent.skills.base import AgentContext, Artifact, Skill, register_skill\n\n\nclass YourSkill(Skill):\n name = \"your_skill\"\n description = \"Generates a custom artifact from the knowledge graph.\"\n\n def execute(self, context: AgentContext, **kwargs) -> Artifact:\n \"\"\"Generate the artifact.\"\"\"\n kg_data = context.knowledge_graph.to_dict()\n # Build content from knowledge graph data\n content = f\"# Your Artifact\\n\\n{len(kg_data.get('entities', []))} entities found.\"\n return Artifact(\n name=\"your_artifact\",\n content=content,\n artifact_type=\"document\",\n format=\"markdown\",\n )\n\n def can_execute(self, context: AgentContext) -> bool:\n \"\"\"Check prerequisites (default requires KG + provider).\"\"\"\n return context.knowledge_graph is not None\n\n\n# Self-registration at import time\nregister_skill(YourSkill())\n</code></pre>"},{"location":"contributing/#registering-in-__init__py_1","title":"Registering in <code>__init__.py</code>","text":"<p>Add the import to <code>video_processor/agent/skills/__init__.py</code> so the skill is loaded (and self-registered) when the skills package is imported:</p> <pre><code>from video_processor.agent.skills import (\n ...\n your_skill, # noqa: F401\n)\n</code></pre>"},{"location":"contributing/#adding-a-new-document-processor","title":"Adding a new document processor","text":"<p>Document processors extend the <code>DocumentProcessor</code> ABC from <code>video_processor/processors/base.py</code> and are registered via <code>register_processor()</code>.</p> <ol> <li>Create <code>video_processor/processors/your_processor.py</code></li> <li>Extend <code>DocumentProcessor</code></li> <li>Set <code>supported_extensions</code> class attribute</li> <li>Implement <code>process()</code> (returns <code>List[DocumentChunk]</code>) and <code>can_process()</code></li> <li>Call <code>register_processor()</code> at module level</li> <li>Add the import to <code>video_processor/processors/__init__.py</code></li> <li>Add tests</li> </ol>"},{"location":"contributing/#example-processor-skeleton","title":"Example processor skeleton","text":"<pre><code>\"\"\"Your document processor.\"\"\"\n\nfrom pathlib import Path\nfrom typing import List\n\nfrom video_processor.processors.base import (\n DocumentChunk,\n DocumentProcessor,\n register_processor,\n)\n\n\nclass YourProcessor(DocumentProcessor):\n supported_extensions = [\".xyz\", \".abc\"]\n\n def can_process(self, path: Path) -> bool:\n return path.suffix.lower() in self.supported_extensions\n\n def process(self, path: Path) -> List[DocumentChunk]:\n text = path.read_text()\n # Split into chunks as appropriate for your format\n return [\n DocumentChunk(\n text=text,\n source_file=str(path),\n chunk_index=0,\n metadata={\"format\": \"xyz\"},\n )\n ]\n\n\n# Self-registration at import time\nregister_processor([\".xyz\", \".abc\"], YourProcessor)\n</code></pre>"},{"location":"contributing/#registering-in-__init__py_2","title":"Registering in <code>__init__.py</code>","text":"<p>Add the import to <code>video_processor/processors/__init__.py</code>:</p> <pre><code>from video_processor.processors import (\n markdown_processor, # noqa: F401, E402\n pdf_processor, # noqa: F401, E402\n your_processor, # noqa: F401, E402\n)\n</code></pre>"},{"location":"contributing/#adding-a-new-exporter","title":"Adding a new exporter","text":"<p>Exporters live in <code>video_processor/exporters/</code> and are typically called from CLI commands. There is no strict ABC for exporters -- they are plain functions that accept knowledge graph data and an output directory.</p> <ol> <li>Create <code>video_processor/exporters/your_exporter.py</code></li> <li>Implement one or more export functions that accept KG data (as a dict) and an output path</li> <li>Add CLI integration in <code>video_processor/cli/commands.py</code> under the <code>export</code> group</li> <li>Add tests</li> </ol>"},{"location":"contributing/#example-exporter-skeleton","title":"Example exporter skeleton","text":"<pre><code>\"\"\"Your exporter.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import List\n\n\ndef export_your_format(kg_data: dict, output_dir: Path) -> List[Path]:\n \"\"\"Export knowledge graph data in your format.\n\n Args:\n kg_data: Knowledge graph as a dict (from KnowledgeGraph.to_dict()).\n output_dir: Directory to write output files.\n\n Returns:\n List of created file paths.\n \"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n created = []\n\n output_file = output_dir / \"export.xyz\"\n output_file.write_text(json.dumps(kg_data, indent=2))\n created.append(output_file)\n\n return created\n</code></pre>"},{"location":"contributing/#adding-the-cli-command","title":"Adding the CLI command","text":"<p>Add a subcommand under the <code>export</code> group in <code>video_processor/cli/commands.py</code>:</p> <pre><code>@export.command(\"your-format\")\[email protected](\"db_path\", type=click.Path(exists=True))\[email protected](\"-o\", \"--output\", type=click.Path(), default=None)\ndef export_your_format_cmd(db_path, output):\n \"\"\"Export knowledge graph in your format.\"\"\"\n from video_processor.exporters.your_exporter import export_your_format\n from video_processor.integrators.knowledge_graph import KnowledgeGraph\n\n kg = KnowledgeGraph(db_path=Path(db_path))\n out_dir = Path(output) if output else Path.cwd() / \"your-export\"\n created = export_your_format(kg.to_dict(), out_dir)\n click.echo(f\"Exported {len(created)} files to {out_dir}/\")\n</code></pre>"},{"location":"contributing/#license","title":"License","text":"<p>MIT License -- Copyright (c) 2026 CONFLICT LLC. All rights reserved.</p>"},{"location":"faq/","title":"FAQ & Troubleshooting","text":""},{"location":"faq/#frequently-asked-questions","title":"Frequently Asked Questions","text":""},{"location":"faq/#do-i-need-an-api-key","title":"Do I need an API key?","text":"<p>You need at least one of:</p> <ul> <li>Cloud API key: <code>OPENAI_API_KEY</code>, <code>ANTHROPIC_API_KEY</code>, or <code>GEMINI_API_KEY</code></li> <li>Local Ollama: Install Ollama, pull a model, and run <code>ollama serve</code></li> </ul> <p>Some features work without any AI provider:</p> <ul> <li><code>planopticon query stats</code> \u2014 direct knowledge graph queries</li> <li><code>planopticon query \"entities --type person\"</code> \u2014 structured entity lookups</li> <li><code>planopticon export markdown</code> \u2014 document generation from existing KG (7 document types, no LLM)</li> <li><code>planopticon kg inspect</code> \u2014 knowledge graph statistics</li> <li><code>planopticon kg convert</code> \u2014 format conversion</li> </ul>"},{"location":"faq/#how-much-does-it-cost","title":"How much does it cost?","text":"<p>PlanOpticon defaults to cheap models to minimize costs:</p> Task Default model Approximate cost Chat/analysis Claude Haiku / GPT-4o-mini ~$0.25-0.50 per 1M tokens Vision (diagrams) Gemini Flash / GPT-4o-mini ~$0.10-0.50 per 1M tokens Transcription Local Whisper (free) / Whisper-1 $0.006/minute <p>A typical 1-hour meeting costs roughly $0.05-0.15 to process with default models. Use <code>--provider ollama</code> for zero cost.</p>"},{"location":"faq/#can-i-run-fully-offline","title":"Can I run fully offline?","text":"<p>Yes. Install Ollama and local Whisper:</p> <pre><code>ollama pull llama3.2\nollama pull llava\npip install planopticon[gpu]\nplanopticon analyze -i video.mp4 -o ./output --provider ollama\n</code></pre> <p>No data leaves your machine.</p>"},{"location":"faq/#what-video-formats-are-supported","title":"What video formats are supported?","text":"<p>Any format FFmpeg can decode:</p> <ul> <li>MP4, MKV, AVI, MOV, WebM, FLV, WMV, M4V</li> <li>Container formats with common codecs (H.264, H.265, VP8, VP9, AV1)</li> </ul>"},{"location":"faq/#what-document-formats-can-i-ingest","title":"What document formats can I ingest?","text":"<ul> <li>PDF \u2014 text extraction via pymupdf or pdfplumber</li> <li>Markdown \u2014 parsed with heading-based chunking</li> <li>Plain text \u2014 paragraph-based chunking with overlap</li> </ul>"},{"location":"faq/#how-does-the-knowledge-graph-work","title":"How does the knowledge graph work?","text":"<p>PlanOpticon extracts entities (people, technologies, concepts, decisions) and relationships from your content. These are stored in a SQLite database (<code>knowledge_graph.db</code>) with zero external dependencies. Entities are automatically classified using a planning taxonomy (goals, requirements, risks, tasks, milestones).</p> <p>When you process multiple sources, entities are merged using fuzzy name matching (0.85 threshold) with type conflict resolution and provenance tracking.</p>"},{"location":"faq/#can-i-use-planopticon-with-my-existing-obsidian-vault","title":"Can I use PlanOpticon with my existing Obsidian vault?","text":"<p>Yes, in both directions:</p> <pre><code># Ingest an Obsidian vault into PlanOpticon\nplanopticon ingest ~/Obsidian/MyVault --output ./kb --recursive\n\n# Export PlanOpticon knowledge to an Obsidian vault\nplanopticon export obsidian --input ./kb --output ~/Obsidian/PlanOpticon\n</code></pre> <p>The Obsidian export produces proper YAML frontmatter, wiki-links (<code>[[Entity Name]]</code>), and tag pages.</p>"},{"location":"faq/#how-do-i-add-my-own-ai-provider","title":"How do I add my own AI provider?","text":"<p>Create a provider module, extend <code>BaseProvider</code>, and register it:</p> <pre><code>from video_processor.providers.base import BaseProvider, ProviderRegistry\n\nclass MyProvider(BaseProvider):\n provider_name = \"myprovider\"\n\n def chat(self, messages, max_tokens=4096, temperature=0.7, model=None):\n # Your implementation\n ...\n\nProviderRegistry.register(\n name=\"myprovider\",\n provider_class=MyProvider,\n env_var=\"MY_PROVIDER_API_KEY\",\n model_prefixes=[\"my-\"],\n default_models={\"chat\": \"my-model-v1\", \"vision\": \"\", \"audio\": \"\"},\n)\n</code></pre> <p>See the Contributing guide for details.</p>"},{"location":"faq/#troubleshooting","title":"Troubleshooting","text":""},{"location":"faq/#authentication-errors","title":"Authentication errors","text":""},{"location":"faq/#no-auth-method-available-for-zoom","title":"\"No auth method available for zoom\"","text":"<p>You need to set credentials before authenticating:</p> <pre><code>export ZOOM_CLIENT_ID=\"your-client-id\"\nexport ZOOM_CLIENT_SECRET=\"your-client-secret\"\nplanopticon auth zoom\n</code></pre> <p>The error message tells you which environment variables to set. Each service requires different credentials \u2014 see the Authentication guide.</p>"},{"location":"faq/#token-expired-or-401-unauthorized","title":"\"Token expired\" or \"401 Unauthorized\"","text":"<p>Your saved token has expired and auto-refresh failed. Re-authenticate:</p> <pre><code>planopticon auth google # or whatever service\n</code></pre> <p>To clear a stale token:</p> <pre><code>planopticon auth google --logout\nplanopticon auth google\n</code></pre> <p>Tokens are stored in <code>~/.planopticon/{service}_token.json</code>.</p>"},{"location":"faq/#oauth-redirect-errors","title":"OAuth redirect errors","text":"<p>If the browser-based OAuth flow fails, check:</p> <ol> <li>Your client ID and secret are correct</li> <li>The redirect URI in your OAuth app matches PlanOpticon's default (<code>urn:ietf:wg:oauth:2.0:oob</code>)</li> <li>The OAuth app has the required scopes enabled</li> </ol>"},{"location":"faq/#provider-errors","title":"Provider errors","text":""},{"location":"faq/#anthropic_api_key-not-set","title":"\"ANTHROPIC_API_KEY not set\"","text":"<p>Set at least one provider's API key:</p> <pre><code>export OPENAI_API_KEY=\"sk-...\"\n# or\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n# or\nexport GEMINI_API_KEY=\"AI...\"\n</code></pre> <p>Or use a <code>.env</code> file in your project directory.</p>"},{"location":"faq/#unexpected-role-system-anthropic","title":"\"Unexpected role system\" (Anthropic)","text":"<p>This was a bug in older versions where system messages were passed in the messages array instead of as a top-level parameter. Update to v0.4.0 or later.</p>"},{"location":"faq/#model-not-found-or-invalid-model","title":"\"Model not found\" or \"Invalid model\"","text":"<p>Check available models:</p> <pre><code>planopticon list-models\n</code></pre> <p>Common model name issues: - Anthropic: use <code>claude-haiku-4-5-20251001</code>, not <code>claude-haiku</code> - OpenAI: use <code>gpt-4o-mini</code>, not <code>gpt4o-mini</code></p>"},{"location":"faq/#rate-limiting-429-errors","title":"Rate limiting / 429 errors","text":"<p>PlanOpticon doesn't currently implement automatic retry. If you hit rate limits:</p> <ol> <li>Use a different provider: <code>--provider gemini</code></li> <li>Use cheaper/faster models: <code>--chat-model gpt-4o-mini</code></li> <li>Reduce processing depth: <code>--depth basic</code></li> <li>Use Ollama for zero rate limits: <code>--provider ollama</code></li> </ol>"},{"location":"faq/#processing-errors","title":"Processing errors","text":""},{"location":"faq/#ffmpeg-not-found","title":"\"FFmpeg not found\"","text":"<p>Install FFmpeg:</p> <pre><code># macOS\nbrew install ffmpeg\n\n# Ubuntu/Debian\nsudo apt-get install ffmpeg libsndfile1\n\n# Windows\n# Download from https://ffmpeg.org/download.html and add to PATH\n</code></pre>"},{"location":"faq/#audio-extraction-failed-no-audio-track-found","title":"\"Audio extraction failed: no audio track found\"","text":"<p>The video file has no audio track. PlanOpticon will skip transcription and continue with frame analysis only.</p>"},{"location":"faq/#frame-extraction-memory-error","title":"\"Frame extraction memory error\"","text":"<p>For very long videos, frame extraction can use significant memory. Use the <code>--max-memory-mb</code> safety valve:</p> <pre><code>planopticon analyze -i long-video.mp4 -o ./output --max-memory-mb 2048\n</code></pre> <p>Or reduce the sampling rate:</p> <pre><code>planopticon analyze -i long-video.mp4 -o ./output --sampling-rate 0.25\n</code></pre>"},{"location":"faq/#batch-processing-one-video-fails","title":"Batch processing \u2014 one video fails","text":"<p>Individual video failures don't stop the batch. Failed videos are logged in the batch manifest with error details. Check <code>batch_manifest.json</code> for the specific error.</p>"},{"location":"faq/#knowledge-graph-issues","title":"Knowledge graph issues","text":""},{"location":"faq/#no-knowledge-graph-loaded-in-companion","title":"\"No knowledge graph loaded\" in companion","text":"<p>The companion auto-discovers knowledge graphs by looking for <code>knowledge_graph.db</code> or <code>knowledge_graph.json</code> in the current directory and parent directories. Either:</p> <ol> <li><code>cd</code> to the directory containing your knowledge graph</li> <li>Specify the path explicitly: <code>planopticon companion --kb ./path/to/kb</code></li> </ol>"},{"location":"faq/#empty-or-sparse-knowledge-graph","title":"Empty or sparse knowledge graph","text":"<p>Common causes:</p> <ol> <li>Too few entities extracted: Try <code>--depth comprehensive</code> for deeper analysis</li> <li>Short or low-quality transcript: Check <code>transcript/transcript.txt</code> \u2014 poor audio produces poor transcription</li> <li>Wrong provider: Some models extract entities better than others. Try <code>--provider openai --chat-model gpt-4o</code> for higher quality</li> </ol>"},{"location":"faq/#duplicate-entities-after-merge","title":"Duplicate entities after merge","text":"<p>The fuzzy matching threshold is 0.85 (SequenceMatcher ratio). If you're getting duplicates, the names are too different for automatic matching. You can manually inspect and merge:</p> <pre><code>planopticon kg inspect ./knowledge_graph.db\nplanopticon query \"entities --name python\"\n</code></pre>"},{"location":"faq/#companion-repl-issues","title":"Companion / REPL issues","text":""},{"location":"faq/#chat-gives-generic-advice-instead-of-project-specific-answers","title":"Chat gives generic advice instead of project-specific answers","text":"<p>The companion needs both a knowledge graph and an LLM provider. Check:</p> <pre><code>planopticon> /status\n</code></pre> <p>If it says \"KG: not loaded\" or \"Provider: none\", fix those first:</p> <pre><code>planopticon> /provider openai\nplanopticon> /model gpt-4o-mini\n</code></pre>"},{"location":"faq/#companion-is-slow","title":"Companion is slow","text":"<p>The companion makes LLM API calls for chat messages. To speed things up:</p> <ol> <li>Use a faster model: <code>/model gpt-4o-mini</code> or <code>/model claude-haiku-4-5-20251001</code></li> <li>Use direct queries instead of chat: <code>/entities</code>, <code>/search</code>, <code>/neighbors</code> don't need an LLM</li> <li>Use Ollama locally for lower latency: <code>/provider ollama</code></li> </ol>"},{"location":"faq/#export-issues","title":"Export issues","text":""},{"location":"faq/#obsidian-export-has-broken-links","title":"Obsidian export has broken links","text":"<p>Make sure your Obsidian vault has wiki-links enabled (Settings > Files & Links > Use [[Wikilinks]]). PlanOpticon exports use wiki-link syntax by default.</p>"},{"location":"faq/#pdf-export-fails","title":"PDF export fails","text":"<p>PDF export requires the <code>pdf</code> extra:</p> <pre><code>pip install planopticon[pdf]\n</code></pre> <p>This installs WeasyPrint, which has system dependencies. On macOS:</p> <pre><code>brew install pango\n</code></pre> <p>On Ubuntu:</p> <pre><code>sudo apt-get install libpango1.0-dev\n</code></pre>"},{"location":"use-cases/","title":"Use Cases","text":"<p>PlanOpticon is built for anyone who needs to turn unstructured content \u2014 recordings, documents, notes, web pages \u2014 into structured, searchable, actionable knowledge. Here are the most common ways people use it.</p>"},{"location":"use-cases/#meeting-notes-and-follow-ups","title":"Meeting notes and follow-ups","text":"<p>Problem: You have hours of meeting recordings but no time to rewatch them. Action items get lost, decisions are forgotten, and new team members have no way to catch up.</p> <p>Solution: Point PlanOpticon at your recordings and get structured transcripts, action items with assignees and deadlines, key decisions, and a knowledge graph linking people to topics.</p> <pre><code># Analyze a single meeting recording\nplanopticon analyze -i standup-2026-03-07.mp4 -o ./meetings/march-7\n\n# Process a month of recordings at once\nplanopticon batch -i ./recordings/march -o ./meetings --title \"March 2026 Meetings\"\n\n# Query what was decided\nplanopticon query \"What decisions were made about the API redesign?\"\n\n# Find all action items assigned to Alice\nplanopticon query \"relationships --source Alice\"\n</code></pre> <p>What you get:</p> <ul> <li>Full transcript with timestamps and speaker segments</li> <li>Action items extracted with assignees, deadlines, and context</li> <li>Key points and decisions highlighted</li> <li>Knowledge graph connecting people, topics, technologies, and decisions</li> <li>Markdown report you can share with the team</li> </ul> <p>Next steps: Export to your team's wiki or note system:</p> <pre><code># Push to GitHub wiki\nplanopticon wiki generate --input ./meetings --output ./wiki\nplanopticon wiki push --input ./wiki --target \"github://your-org/your-repo\"\n\n# Export to Obsidian for personal knowledge management\nplanopticon export obsidian --input ./meetings --output ~/Obsidian/Meetings\n</code></pre>"},{"location":"use-cases/#research-processing","title":"Research processing","text":"<p>Problem: You're researching a topic across YouTube talks, arXiv papers, blog posts, and podcasts. Information is scattered and hard to cross-reference.</p> <p>Solution: Ingest everything into a single knowledge graph, then query across all sources.</p> <pre><code># Ingest a YouTube conference talk\nplanopticon ingest \"https://youtube.com/watch?v=...\" --output ./research\n\n# Ingest arXiv papers\nplanopticon ingest \"https://arxiv.org/abs/2401.12345\" --output ./research\n\n# Ingest blog posts and documentation\nplanopticon ingest \"https://example.com/blog/post\" --output ./research\n\n# Ingest local PDF papers\nplanopticon ingest ./papers/ --output ./research --recursive\n\n# Now query across everything\nplanopticon query \"What approaches to vector search were discussed?\"\nplanopticon query \"entities --type technology\"\nplanopticon query \"neighbors TransformerArchitecture\"\n</code></pre> <p>What you get:</p> <ul> <li>A unified knowledge graph merging entities across all sources</li> <li>Cross-references showing where the same concept appears in different sources</li> <li>Searchable entity index by type (people, technologies, concepts, papers)</li> <li>Relationship maps showing how ideas connect</li> </ul> <p>Go deeper with the companion:</p> <pre><code>planopticon companion --kb ./research\n</code></pre> <pre><code>planopticon> What are the main approaches to retrieval-augmented generation?\nplanopticon> /entities --type technology\nplanopticon> /neighbors RAG\nplanopticon> /export obsidian\n</code></pre>"},{"location":"use-cases/#knowledge-gathering-across-platforms","title":"Knowledge gathering across platforms","text":"<p>Problem: Your team's knowledge is spread across Google Docs, Notion, Obsidian, GitHub wikis, and Apple Notes. There's no single place to search everything.</p> <p>Solution: Pull from all sources into one knowledge graph.</p> <pre><code># Authenticate with your platforms\nplanopticon auth google\nplanopticon auth notion\nplanopticon auth github\n\n# Ingest from Google Workspace\nplanopticon gws ingest --folder-id abc123 --output ./kb --recursive\n\n# Ingest from Notion\nplanopticon ingest --source notion --output ./kb\n\n# Ingest from an Obsidian vault\nplanopticon ingest ~/Obsidian/WorkVault --output ./kb --recursive\n\n# Ingest from GitHub wikis and READMEs\nplanopticon ingest \"github://your-org/project-a\" --output ./kb\nplanopticon ingest \"github://your-org/project-b\" --output ./kb\n\n# Query the unified knowledge base\nplanopticon query stats\nplanopticon query \"entities --type person\"\nplanopticon query \"What do we know about the authentication system?\"\n</code></pre> <p>What you get:</p> <ul> <li>Merged knowledge graph with provenance tracking (you can see which source each entity came from)</li> <li>Deduplicated entities across platforms (same concept mentioned in Notion and Google Docs gets merged)</li> <li>Full-text search across all ingested content</li> <li>Relationship maps showing how concepts connect across your organization's documents</li> </ul>"},{"location":"use-cases/#team-onboarding","title":"Team onboarding","text":"<p>Problem: New team members spend weeks reading docs, watching recorded meetings, and asking questions to get up to speed.</p> <p>Solution: Build a knowledge base from existing content and let new people explore it conversationally.</p> <pre><code># Build the knowledge base from everything\nplanopticon batch -i ./recordings/onboarding -o ./kb --title \"Team Onboarding\"\nplanopticon ingest ./docs/ --output ./kb --recursive\nplanopticon ingest ./architecture-decisions/ --output ./kb --recursive\n\n# New team member launches the companion\nplanopticon companion --kb ./kb\n</code></pre> <pre><code>planopticon> What is the overall architecture of the system?\nplanopticon> Who are the key people on the team?\nplanopticon> /entities --type technology\nplanopticon> What was the rationale for choosing PostgreSQL over MongoDB?\nplanopticon> /neighbors AuthenticationService\nplanopticon> What are the main open issues or risks?\n</code></pre> <p>What you get:</p> <ul> <li>Interactive Q&A over the entire team knowledge base</li> <li>Entity exploration \u2014 browse people, technologies, services, decisions</li> <li>Relationship navigation \u2014 \"show me everything connected to the payment system\"</li> <li>No need to rewatch hours of recordings</li> </ul>"},{"location":"use-cases/#data-collection-and-synthesis","title":"Data collection and synthesis","text":"<p>Problem: You need to collect and synthesize information from many sources \u2014 customer interviews, competitor analysis, market research \u2014 into a coherent picture.</p> <p>Solution: Batch process recordings and documents, then use the planning agent to generate synthesis artifacts.</p> <pre><code># Process customer interview recordings\nplanopticon batch -i ./interviews -o ./research --title \"Customer Interviews Q1\"\n\n# Ingest competitor documentation\nplanopticon ingest ./competitor-analysis/ --output ./research --recursive\n\n# Ingest market research PDFs\nplanopticon ingest ./market-reports/ --output ./research --recursive\n\n# Use the planning agent to synthesize\nplanopticon agent --kb ./research --interactive\n</code></pre> <pre><code>planopticon> Generate a summary of common customer pain points\nplanopticon> /plan\nplanopticon> /tasks\nplanopticon> /export markdown\n</code></pre> <p>What you get:</p> <ul> <li>Merged knowledge graph across all interviews and documents</li> <li>Cross-referenced entities showing which customers mentioned which features</li> <li>Agent-generated project plans, PRDs, and task breakdowns based on the data</li> <li>Exportable artifacts for sharing with stakeholders</li> </ul>"},{"location":"use-cases/#content-creation-from-video","title":"Content creation from video","text":"<p>Problem: You have video content (lectures, tutorials, webinars) that you want to turn into written documentation, blog posts, or course materials.</p> <p>Solution: Extract structured knowledge and export it in your preferred format.</p> <pre><code># Analyze the video\nplanopticon analyze -i webinar-recording.mp4 -o ./content\n\n# Generate multiple document types (no LLM needed for these)\nplanopticon export markdown --input ./content --output ./docs\n\n# Export to Obsidian for further editing\nplanopticon export obsidian --input ./content --output ~/Obsidian/Content\n</code></pre> <p>What you get for each video:</p> <ul> <li>Full transcript (JSON, plain text, SRT subtitles)</li> <li>Extracted diagrams reproduced as Mermaid/SVG/PNG</li> <li>Charts reproduced with data tables</li> <li>Knowledge graph of concepts and relationships</li> <li>7 types of markdown documents: summary, meeting notes, glossary, relationship map, status report, entity index, CSV data</li> </ul>"},{"location":"use-cases/#decision-tracking-over-time","title":"Decision tracking over time","text":"<p>Problem: Important decisions are made in meetings but never formally recorded. Months later, nobody remembers why a choice was made.</p> <p>Solution: Process meeting recordings continuously and query the growing knowledge graph for decisions and their context.</p> <pre><code># Process each week's recordings\nplanopticon batch -i ./recordings/week-12 -o ./decisions --title \"Week 12\"\n\n# The knowledge graph grows over time \u2014 entities merge across weeks\nplanopticon query \"entities --type goal\"\nplanopticon query \"entities --type risk\"\nplanopticon query \"entities --type milestone\"\n\n# Find decisions about a specific topic\nplanopticon query \"What was decided about the database migration?\"\n\n# Track risks over time\nplanopticon query \"relationships --type risk\"\n</code></pre> <p>The planning taxonomy automatically classifies entities as goals, requirements, risks, tasks, and milestones \u2014 giving you a structured view of project evolution over time.</p>"},{"location":"use-cases/#zoom-teams-meet-integration","title":"Zoom / Teams / Meet integration","text":"<p>Problem: Meeting recordings are sitting in Zoom/Teams/Meet cloud storage. You want to process them without manually downloading each one.</p> <p>Solution: Authenticate once, list recordings, and process them directly.</p> <pre><code># Authenticate with your meeting platform\nplanopticon auth zoom\n# or: planopticon auth microsoft\n# or: planopticon auth google\n\n# List recent recordings\nplanopticon recordings zoom-list\nplanopticon recordings teams-list --from 2026-01-01\nplanopticon recordings meet-list --limit 20\n\n# Process recordings (download + analyze)\nplanopticon analyze -i \"zoom://recording-id\" -o ./output\n</code></pre> <p>Setup requirements:</p> Platform What you need Zoom <code>ZOOM_CLIENT_ID</code> + <code>ZOOM_CLIENT_SECRET</code> (create an OAuth app at marketplace.zoom.us) Teams <code>MICROSOFT_CLIENT_ID</code> + <code>MICROSOFT_CLIENT_SECRET</code> (register an Azure AD app) Meet <code>GOOGLE_CLIENT_ID</code> + <code>GOOGLE_CLIENT_SECRET</code> (create OAuth credentials in Google Cloud Console) <p>See the Authentication guide for detailed setup instructions.</p>"},{"location":"use-cases/#fully-offline-processing","title":"Fully offline processing","text":"<p>Problem: You're working with sensitive content that can't leave your network, or you simply don't want to pay for API calls.</p> <p>Solution: Use Ollama for local AI processing with no external API calls.</p> <pre><code># Install Ollama and pull models\nollama pull llama3.2 # Chat/analysis\nollama pull llava # Vision (diagram detection)\n\n# Install local Whisper for transcription\npip install planopticon[gpu]\n\n# Process entirely offline\nplanopticon analyze -i sensitive-meeting.mp4 -o ./output --provider ollama\n</code></pre> <p>PlanOpticon auto-detects Ollama when it's running. If no cloud API keys are configured, it uses Ollama automatically. Pair with local Whisper transcription for a fully air-gapped pipeline.</p>"},{"location":"use-cases/#competitive-research","title":"Competitive research","text":"<p>Problem: You want to systematically analyze competitor content \u2014 conference talks, documentation, blog posts \u2014 and identify patterns.</p> <p>Solution: Ingest competitor content from multiple sources and query for patterns.</p> <pre><code># Ingest competitor conference talks from YouTube\nplanopticon ingest \"https://youtube.com/watch?v=competitor-talk-1\" --output ./competitive\nplanopticon ingest \"https://youtube.com/watch?v=competitor-talk-2\" --output ./competitive\n\n# Ingest their documentation\nplanopticon ingest \"https://competitor.com/docs\" --output ./competitive\n\n# Ingest their GitHub repos\nplanopticon auth github\nplanopticon ingest \"github://competitor/main-product\" --output ./competitive\n\n# Analyze patterns\nplanopticon query \"entities --type technology\"\nplanopticon query \"What technologies are competitors investing in?\"\nplanopticon companion --kb ./competitive\n</code></pre> <pre><code>planopticon> What are the common architectural patterns across competitors?\nplanopticon> /entities --type technology\nplanopticon> Which technologies appear most frequently?\nplanopticon> /export markdown\n</code></pre>"},{"location":"api/agent/","title":"Agent API Reference","text":""},{"location":"api/agent/#video_processor.agent.agent_loop","title":"<code>video_processor.agent.agent_loop</code>","text":"<p>Planning agent loop for synthesizing knowledge into artifacts.</p>"},{"location":"api/agent/#video_processor.agent.agent_loop.PlanningAgent","title":"<code>PlanningAgent</code>","text":"<p>AI agent that synthesizes knowledge into planning artifacts.</p> Source code in <code>video_processor/agent/agent_loop.py</code> <pre><code>class PlanningAgent:\n \"\"\"AI agent that synthesizes knowledge into planning artifacts.\"\"\"\n\n def __init__(self, context: AgentContext):\n self.context = context\n\n @classmethod\n def from_kb_paths(cls, kb_paths: List[Path], provider_manager=None) -> \"PlanningAgent\":\n \"\"\"Create an agent from knowledge base paths.\"\"\"\n kb = KBContext()\n for path in kb_paths:\n kb.add_source(path)\n kb.load(provider_manager=provider_manager)\n\n context = AgentContext(\n knowledge_graph=kb.knowledge_graph,\n query_engine=kb.query_engine,\n provider_manager=provider_manager,\n )\n return cls(context)\n\n def execute(self, request: str) -> List[Artifact]:\n \"\"\"Execute a user request by selecting and running appropriate skills.\"\"\"\n # Step 1: Build context summary for LLM\n kb_summary = \"\"\n if self.context.query_engine:\n stats = self.context.query_engine.stats()\n kb_summary = stats.to_text()\n\n available_skills = list_skills()\n skill_descriptions = \"\\n\".join(f\"- {s.name}: {s.description}\" for s in available_skills)\n\n # Step 2: Ask LLM to select skills\n plan_prompt = (\n \"You are a planning agent. Given a user request and available skills, \"\n \"select which skills to execute and in what order.\\n\\n\"\n f\"Knowledge base:\\n{kb_summary}\\n\\n\"\n f\"Available skills:\\n{skill_descriptions}\\n\\n\"\n f\"User request: {request}\\n\\n\"\n \"Return a JSON array of skill names to execute in order:\\n\"\n '[{\"skill\": \"skill_name\", \"params\": {}}]\\n'\n \"Return ONLY the JSON array.\"\n )\n\n if not self.context.provider_manager:\n # No LLM -- try to match skills by keyword\n return self._keyword_match_execute(request)\n\n raw = self.context.provider_manager.chat(\n [{\"role\": \"user\", \"content\": plan_prompt}],\n max_tokens=512,\n temperature=0.1,\n )\n\n from video_processor.utils.json_parsing import parse_json_from_response\n\n plan = parse_json_from_response(raw)\n\n artifacts = []\n if isinstance(plan, list):\n for step in plan:\n if isinstance(step, dict) and \"skill\" in step:\n skill = get_skill(step[\"skill\"])\n if skill and skill.can_execute(self.context):\n params = step.get(\"params\", {})\n artifact = skill.execute(self.context, **params)\n artifacts.append(artifact)\n self.context.artifacts.append(artifact)\n\n return artifacts\n\n def _keyword_match_execute(self, request: str) -> List[Artifact]:\n \"\"\"Fallback: match skills by keywords in the request.\"\"\"\n request_lower = request.lower()\n artifacts = []\n for skill in list_skills():\n # Simple keyword matching\n skill_words = skill.name.replace(\"_\", \" \").split()\n if any(word in request_lower for word in skill_words):\n if skill.can_execute(self.context):\n artifact = skill.execute(self.context)\n artifacts.append(artifact)\n self.context.artifacts.append(artifact)\n return artifacts\n\n def chat(self, message: str) -> str:\n \"\"\"Interactive chat -- accumulate context and answer questions.\"\"\"\n self.context.conversation_history.append({\"role\": \"user\", \"content\": message})\n\n if not self.context.provider_manager:\n return \"Agent requires a configured LLM provider for chat mode.\"\n\n # Build system context\n kb_summary = \"\"\n if self.context.query_engine:\n stats = self.context.query_engine.stats()\n kb_summary = f\"\\n\\nKnowledge base:\\n{stats.to_text()}\"\n\n artifacts_summary = \"\"\n if self.context.artifacts:\n artifacts_summary = \"\\n\\nGenerated artifacts:\\n\" + \"\\n\".join(\n f\"- {a.name} ({a.artifact_type})\" for a in self.context.artifacts\n )\n\n system_msg = (\n \"You are PlanOpticon, an AI planning companion built into the PlanOpticon CLI. \"\n \"PlanOpticon is a video analysis and knowledge extraction tool that processes \"\n \"recordings into structured knowledge graphs.\\n\\n\"\n \"You are running inside the interactive companion REPL. The user can use these \"\n \"built-in commands (suggest them when relevant):\\n\"\n \" /status - Show workspace status (loaded KG, videos, docs)\\n\"\n \" /entities [--type T] - List knowledge graph entities\\n\"\n \" /search TERM - Search entities by name\\n\"\n \" /neighbors ENTITY - Show entity relationships\\n\"\n \" /export FORMAT - Export KG (markdown, obsidian, notion, csv)\\n\"\n \" /analyze PATH - Analyze a video or document\\n\"\n \" /ingest PATH - Ingest a file into the knowledge graph\\n\"\n \" /auth SERVICE - Authenticate with a service \"\n \"(zoom, google, microsoft, notion, dropbox, github)\\n\"\n \" /provider [NAME] - List or switch LLM provider\\n\"\n \" /model [NAME] - Show or switch chat model\\n\"\n \" /plan - Generate a project plan\\n\"\n \" /prd - Generate a PRD\\n\"\n \" /tasks - Generate a task breakdown\\n\\n\"\n \"PlanOpticon CLI commands the user can run outside the REPL:\\n\"\n \" planopticon auth zoom|google|microsoft - Authenticate with cloud services\\n\"\n \" planopticon recordings zoom-list|teams-list|meet-list - List cloud recordings\\n\"\n \" planopticon analyze -i VIDEO - Analyze a video file\\n\"\n \" planopticon query - Query the knowledge graph\\n\"\n \" planopticon export FORMAT PATH - Export knowledge graph\\n\\n\"\n f\"{kb_summary}{artifacts_summary}\\n\\n\"\n \"Help the user with their planning tasks. When they ask about capabilities, \"\n \"refer them to the appropriate built-in commands. Ask clarifying questions \"\n \"to gather requirements. When ready, suggest using specific skills or commands \"\n \"to generate artifacts.\"\n )\n\n messages = [{\"role\": \"system\", \"content\": system_msg}] + self.context.conversation_history\n\n response = self.context.provider_manager.chat(messages, max_tokens=2048, temperature=0.5)\n self.context.conversation_history.append({\"role\": \"assistant\", \"content\": response})\n return response\n</code></pre>"},{"location":"api/agent/#video_processor.agent.agent_loop.PlanningAgent.chat","title":"<code>chat(message)</code>","text":"<p>Interactive chat -- accumulate context and answer questions.</p> Source code in <code>video_processor/agent/agent_loop.py</code> <pre><code>def chat(self, message: str) -> str:\n \"\"\"Interactive chat -- accumulate context and answer questions.\"\"\"\n self.context.conversation_history.append({\"role\": \"user\", \"content\": message})\n\n if not self.context.provider_manager:\n return \"Agent requires a configured LLM provider for chat mode.\"\n\n # Build system context\n kb_summary = \"\"\n if self.context.query_engine:\n stats = self.context.query_engine.stats()\n kb_summary = f\"\\n\\nKnowledge base:\\n{stats.to_text()}\"\n\n artifacts_summary = \"\"\n if self.context.artifacts:\n artifacts_summary = \"\\n\\nGenerated artifacts:\\n\" + \"\\n\".join(\n f\"- {a.name} ({a.artifact_type})\" for a in self.context.artifacts\n )\n\n system_msg = (\n \"You are PlanOpticon, an AI planning companion built into the PlanOpticon CLI. \"\n \"PlanOpticon is a video analysis and knowledge extraction tool that processes \"\n \"recordings into structured knowledge graphs.\\n\\n\"\n \"You are running inside the interactive companion REPL. The user can use these \"\n \"built-in commands (suggest them when relevant):\\n\"\n \" /status - Show workspace status (loaded KG, videos, docs)\\n\"\n \" /entities [--type T] - List knowledge graph entities\\n\"\n \" /search TERM - Search entities by name\\n\"\n \" /neighbors ENTITY - Show entity relationships\\n\"\n \" /export FORMAT - Export KG (markdown, obsidian, notion, csv)\\n\"\n \" /analyze PATH - Analyze a video or document\\n\"\n \" /ingest PATH - Ingest a file into the knowledge graph\\n\"\n \" /auth SERVICE - Authenticate with a service \"\n \"(zoom, google, microsoft, notion, dropbox, github)\\n\"\n \" /provider [NAME] - List or switch LLM provider\\n\"\n \" /model [NAME] - Show or switch chat model\\n\"\n \" /plan - Generate a project plan\\n\"\n \" /prd - Generate a PRD\\n\"\n \" /tasks - Generate a task breakdown\\n\\n\"\n \"PlanOpticon CLI commands the user can run outside the REPL:\\n\"\n \" planopticon auth zoom|google|microsoft - Authenticate with cloud services\\n\"\n \" planopticon recordings zoom-list|teams-list|meet-list - List cloud recordings\\n\"\n \" planopticon analyze -i VIDEO - Analyze a video file\\n\"\n \" planopticon query - Query the knowledge graph\\n\"\n \" planopticon export FORMAT PATH - Export knowledge graph\\n\\n\"\n f\"{kb_summary}{artifacts_summary}\\n\\n\"\n \"Help the user with their planning tasks. When they ask about capabilities, \"\n \"refer them to the appropriate built-in commands. Ask clarifying questions \"\n \"to gather requirements. When ready, suggest using specific skills or commands \"\n \"to generate artifacts.\"\n )\n\n messages = [{\"role\": \"system\", \"content\": system_msg}] + self.context.conversation_history\n\n response = self.context.provider_manager.chat(messages, max_tokens=2048, temperature=0.5)\n self.context.conversation_history.append({\"role\": \"assistant\", \"content\": response})\n return response\n</code></pre>"},{"location":"api/agent/#video_processor.agent.agent_loop.PlanningAgent.execute","title":"<code>execute(request)</code>","text":"<p>Execute a user request by selecting and running appropriate skills.</p> Source code in <code>video_processor/agent/agent_loop.py</code> <pre><code>def execute(self, request: str) -> List[Artifact]:\n \"\"\"Execute a user request by selecting and running appropriate skills.\"\"\"\n # Step 1: Build context summary for LLM\n kb_summary = \"\"\n if self.context.query_engine:\n stats = self.context.query_engine.stats()\n kb_summary = stats.to_text()\n\n available_skills = list_skills()\n skill_descriptions = \"\\n\".join(f\"- {s.name}: {s.description}\" for s in available_skills)\n\n # Step 2: Ask LLM to select skills\n plan_prompt = (\n \"You are a planning agent. Given a user request and available skills, \"\n \"select which skills to execute and in what order.\\n\\n\"\n f\"Knowledge base:\\n{kb_summary}\\n\\n\"\n f\"Available skills:\\n{skill_descriptions}\\n\\n\"\n f\"User request: {request}\\n\\n\"\n \"Return a JSON array of skill names to execute in order:\\n\"\n '[{\"skill\": \"skill_name\", \"params\": {}}]\\n'\n \"Return ONLY the JSON array.\"\n )\n\n if not self.context.provider_manager:\n # No LLM -- try to match skills by keyword\n return self._keyword_match_execute(request)\n\n raw = self.context.provider_manager.chat(\n [{\"role\": \"user\", \"content\": plan_prompt}],\n max_tokens=512,\n temperature=0.1,\n )\n\n from video_processor.utils.json_parsing import parse_json_from_response\n\n plan = parse_json_from_response(raw)\n\n artifacts = []\n if isinstance(plan, list):\n for step in plan:\n if isinstance(step, dict) and \"skill\" in step:\n skill = get_skill(step[\"skill\"])\n if skill and skill.can_execute(self.context):\n params = step.get(\"params\", {})\n artifact = skill.execute(self.context, **params)\n artifacts.append(artifact)\n self.context.artifacts.append(artifact)\n\n return artifacts\n</code></pre>"},{"location":"api/agent/#video_processor.agent.agent_loop.PlanningAgent.from_kb_paths","title":"<code>from_kb_paths(kb_paths, provider_manager=None)</code> <code>classmethod</code>","text":"<p>Create an agent from knowledge base paths.</p> Source code in <code>video_processor/agent/agent_loop.py</code> <pre><code>@classmethod\ndef from_kb_paths(cls, kb_paths: List[Path], provider_manager=None) -> \"PlanningAgent\":\n \"\"\"Create an agent from knowledge base paths.\"\"\"\n kb = KBContext()\n for path in kb_paths:\n kb.add_source(path)\n kb.load(provider_manager=provider_manager)\n\n context = AgentContext(\n knowledge_graph=kb.knowledge_graph,\n query_engine=kb.query_engine,\n provider_manager=provider_manager,\n )\n return cls(context)\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base","title":"<code>video_processor.agent.skills.base</code>","text":"<p>Skill interface for the PlanOpticon planning agent.</p>"},{"location":"api/agent/#video_processor.agent.skills.base.AgentContext","title":"<code>AgentContext</code> <code>dataclass</code>","text":"<p>Shared context for agent skills.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>@dataclass\nclass AgentContext:\n \"\"\"Shared context for agent skills.\"\"\"\n\n knowledge_graph: Any = None # KnowledgeGraph instance\n query_engine: Any = None # GraphQueryEngine instance\n provider_manager: Any = None # ProviderManager instance\n planning_entities: List[Any] = field(default_factory=list)\n user_requirements: Dict[str, Any] = field(default_factory=dict)\n conversation_history: List[Dict[str, str]] = field(default_factory=list)\n artifacts: List[Artifact] = field(default_factory=list)\n config: Dict[str, Any] = field(default_factory=dict)\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.Artifact","title":"<code>Artifact</code> <code>dataclass</code>","text":"<p>Output from a skill execution.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>@dataclass\nclass Artifact:\n \"\"\"Output from a skill execution.\"\"\"\n\n name: str\n content: str # The generated content (markdown, json, etc.)\n artifact_type: str # \"project_plan\", \"prd\", \"roadmap\", \"task_list\", \"document\", \"issues\"\n format: str = \"markdown\" # \"markdown\", \"json\", \"mermaid\"\n metadata: Dict[str, Any] = field(default_factory=dict)\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.Skill","title":"<code>Skill</code>","text":"<p> Bases: <code>ABC</code></p> <p>Base class for agent skills.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>class Skill(ABC):\n \"\"\"Base class for agent skills.\"\"\"\n\n name: str = \"\"\n description: str = \"\"\n\n @abstractmethod\n def execute(self, context: AgentContext, **kwargs) -> Artifact:\n \"\"\"Execute this skill and return an artifact.\"\"\"\n ...\n\n def can_execute(self, context: AgentContext) -> bool:\n \"\"\"Check if this skill can execute given the current context.\"\"\"\n return context.knowledge_graph is not None and context.provider_manager is not None\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.Skill.can_execute","title":"<code>can_execute(context)</code>","text":"<p>Check if this skill can execute given the current context.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>def can_execute(self, context: AgentContext) -> bool:\n \"\"\"Check if this skill can execute given the current context.\"\"\"\n return context.knowledge_graph is not None and context.provider_manager is not None\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.Skill.execute","title":"<code>execute(context, **kwargs)</code> <code>abstractmethod</code>","text":"<p>Execute this skill and return an artifact.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>@abstractmethod\ndef execute(self, context: AgentContext, **kwargs) -> Artifact:\n \"\"\"Execute this skill and return an artifact.\"\"\"\n ...\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.get_skill","title":"<code>get_skill(name)</code>","text":"<p>Look up a skill by name.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>def get_skill(name: str) -> Optional[\"Skill\"]:\n \"\"\"Look up a skill by name.\"\"\"\n return _skills.get(name)\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.list_skills","title":"<code>list_skills()</code>","text":"<p>Return all registered skills.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>def list_skills() -> List[\"Skill\"]:\n \"\"\"Return all registered skills.\"\"\"\n return list(_skills.values())\n</code></pre>"},{"location":"api/agent/#video_processor.agent.skills.base.register_skill","title":"<code>register_skill(skill)</code>","text":"<p>Register a skill instance in the global registry.</p> Source code in <code>video_processor/agent/skills/base.py</code> <pre><code>def register_skill(skill: \"Skill\") -> None:\n \"\"\"Register a skill instance in the global registry.\"\"\"\n _skills[skill.name] = skill\n</code></pre>"},{"location":"api/agent/#video_processor.agent.kb_context","title":"<code>video_processor.agent.kb_context</code>","text":"<p>Knowledge base context manager for loading and merging knowledge graphs.</p>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext","title":"<code>KBContext</code>","text":"<p>Load and merge multiple knowledge graphs into a unified context.</p> Source code in <code>video_processor/agent/kb_context.py</code> <pre><code>class KBContext:\n \"\"\"Load and merge multiple knowledge graphs into a unified context.\"\"\"\n\n def __init__(self):\n self._sources: List[Path] = []\n self._kg = None # KnowledgeGraph instance\n self._engine = None # GraphQueryEngine instance\n\n def add_source(self, path) -> None:\n \"\"\"Add a knowledge graph source (.db or .json file, or directory to search).\"\"\"\n path = Path(path).resolve()\n if path.is_dir():\n from video_processor.integrators.graph_discovery import find_knowledge_graphs\n\n graphs = find_knowledge_graphs(path)\n self._sources.extend(graphs)\n elif path.is_file():\n self._sources.append(path)\n else:\n raise FileNotFoundError(f\"Not found: {path}\")\n\n def load(self, provider_manager=None) -> \"KBContext\":\n \"\"\"Load and merge all added sources into a single knowledge graph.\"\"\"\n from video_processor.integrators.graph_query import GraphQueryEngine\n from video_processor.integrators.knowledge_graph import KnowledgeGraph\n\n self._kg = KnowledgeGraph(provider_manager=provider_manager)\n\n for source_path in self._sources:\n if source_path.suffix == \".db\":\n other = KnowledgeGraph(db_path=source_path)\n self._kg.merge(other)\n elif source_path.suffix == \".json\":\n data = json.loads(source_path.read_text())\n other = KnowledgeGraph.from_dict(data)\n self._kg.merge(other)\n\n self._engine = GraphQueryEngine(self._kg._store, provider_manager=provider_manager)\n return self\n\n @property\n def knowledge_graph(self):\n \"\"\"Return the merged KnowledgeGraph, or None if not loaded.\"\"\"\n if not self._kg:\n raise RuntimeError(\"Call load() first\")\n return self._kg\n\n @property\n def query_engine(self):\n \"\"\"Return the GraphQueryEngine, or None if not loaded.\"\"\"\n if not self._engine:\n raise RuntimeError(\"Call load() first\")\n return self._engine\n\n @property\n def sources(self) -> List[Path]:\n \"\"\"Return the list of source paths.\"\"\"\n return list(self._sources)\n\n def summary(self) -> str:\n \"\"\"Generate a brief summary of the loaded knowledge base.\"\"\"\n if not self._kg:\n return \"No knowledge base loaded.\"\n\n stats = self._engine.stats().data\n lines = [\n f\"Knowledge base: {len(self._sources)} source(s)\",\n f\" Entities: {stats['entity_count']}\",\n f\" Relationships: {stats['relationship_count']}\",\n ]\n if stats.get(\"entity_types\"):\n lines.append(\" Entity types:\")\n for t, count in sorted(stats[\"entity_types\"].items(), key=lambda x: -x[1]):\n lines.append(f\" {t}: {count}\")\n return \"\\n\".join(lines)\n\n @classmethod\n def auto_discover(cls, start_dir: Optional[Path] = None, provider_manager=None) -> \"KBContext\":\n \"\"\"Create a KBContext by auto-discovering knowledge graphs near start_dir.\"\"\"\n from video_processor.integrators.graph_discovery import find_knowledge_graphs\n\n ctx = cls()\n graphs = find_knowledge_graphs(start_dir)\n for g in graphs:\n ctx._sources.append(g)\n if ctx._sources:\n ctx.load(provider_manager=provider_manager)\n return ctx\n</code></pre>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.knowledge_graph","title":"<code>knowledge_graph</code> <code>property</code>","text":"<p>Return the merged KnowledgeGraph, or None if not loaded.</p>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.query_engine","title":"<code>query_engine</code> <code>property</code>","text":"<p>Return the GraphQueryEngine, or None if not loaded.</p>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.sources","title":"<code>sources</code> <code>property</code>","text":"<p>Return the list of source paths.</p>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.add_source","title":"<code>add_source(path)</code>","text":"<p>Add a knowledge graph source (.db or .json file, or directory to search).</p> Source code in <code>video_processor/agent/kb_context.py</code> <pre><code>def add_source(self, path) -> None:\n \"\"\"Add a knowledge graph source (.db or .json file, or directory to search).\"\"\"\n path = Path(path).resolve()\n if path.is_dir():\n from video_processor.integrators.graph_discovery import find_knowledge_graphs\n\n graphs = find_knowledge_graphs(path)\n self._sources.extend(graphs)\n elif path.is_file():\n self._sources.append(path)\n else:\n raise FileNotFoundError(f\"Not found: {path}\")\n</code></pre>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.auto_discover","title":"<code>auto_discover(start_dir=None, provider_manager=None)</code> <code>classmethod</code>","text":"<p>Create a KBContext by auto-discovering knowledge graphs near start_dir.</p> Source code in <code>video_processor/agent/kb_context.py</code> <pre><code>@classmethod\ndef auto_discover(cls, start_dir: Optional[Path] = None, provider_manager=None) -> \"KBContext\":\n \"\"\"Create a KBContext by auto-discovering knowledge graphs near start_dir.\"\"\"\n from video_processor.integrators.graph_discovery import find_knowledge_graphs\n\n ctx = cls()\n graphs = find_knowledge_graphs(start_dir)\n for g in graphs:\n ctx._sources.append(g)\n if ctx._sources:\n ctx.load(provider_manager=provider_manager)\n return ctx\n</code></pre>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.load","title":"<code>load(provider_manager=None)</code>","text":"<p>Load and merge all added sources into a single knowledge graph.</p> Source code in <code>video_processor/agent/kb_context.py</code> <pre><code>def load(self, provider_manager=None) -> \"KBContext\":\n \"\"\"Load and merge all added sources into a single knowledge graph.\"\"\"\n from video_processor.integrators.graph_query import GraphQueryEngine\n from video_processor.integrators.knowledge_graph import KnowledgeGraph\n\n self._kg = KnowledgeGraph(provider_manager=provider_manager)\n\n for source_path in self._sources:\n if source_path.suffix == \".db\":\n other = KnowledgeGraph(db_path=source_path)\n self._kg.merge(other)\n elif source_path.suffix == \".json\":\n data = json.loads(source_path.read_text())\n other = KnowledgeGraph.from_dict(data)\n self._kg.merge(other)\n\n self._engine = GraphQueryEngine(self._kg._store, provider_manager=provider_manager)\n return self\n</code></pre>"},{"location":"api/agent/#video_processor.agent.kb_context.KBContext.summary","title":"<code>summary()</code>","text":"<p>Generate a brief summary of the loaded knowledge base.</p> Source code in <code>video_processor/agent/kb_context.py</code> <pre><code>def summary(self) -> str:\n \"\"\"Generate a brief summary of the loaded knowledge base.\"\"\"\n if not self._kg:\n return \"No knowledge base loaded.\"\n\n stats = self._engine.stats().data\n lines = [\n f\"Knowledge base: {len(self._sources)} source(s)\",\n f\" Entities: {stats['entity_count']}\",\n f\" Relationships: {stats['relationship_count']}\",\n ]\n if stats.get(\"entity_types\"):\n lines.append(\" Entity types:\")\n for t, count in sorted(stats[\"entity_types\"].items(), key=lambda x: -x[1]):\n lines.append(f\" {t}: {count}\")\n return \"\\n\".join(lines)\n</code></pre>"},{"location":"api/agent/#overview","title":"Overview","text":"<p>The agent module implements a planning agent that synthesizes knowledge from processed video content into actionable artifacts such as project plans, PRDs, task breakdowns, and roadmaps. The agent operates on knowledge graphs loaded via <code>KBContext</code> and uses a skill-based architecture for extensibility.</p> <p>Key components:</p> <ul> <li><code>PlanningAgent</code> -- orchestrates skill selection and execution based on user requests</li> <li><code>AgentContext</code> -- shared state passed between skills during execution</li> <li><code>Skill</code> (ABC) -- base class for pluggable agent capabilities</li> <li><code>Artifact</code> -- output produced by skill execution</li> <li><code>KBContext</code> -- loads and merges multiple knowledge graph sources</li> </ul>"},{"location":"api/agent/#planningagent","title":"PlanningAgent","text":"<pre><code>from video_processor.agent.agent_loop import PlanningAgent\n</code></pre> <p>AI agent that synthesizes knowledge into planning artifacts. Uses an LLM to select which skills to execute for a given request, or falls back to keyword matching when no LLM is available.</p>"},{"location":"api/agent/#constructor","title":"Constructor","text":"<pre><code>def __init__(self, context: AgentContext)\n</code></pre> Parameter Type Description <code>context</code> <code>AgentContext</code> Shared context containing knowledge graph, query engine, and provider"},{"location":"api/agent/#from_kb_paths","title":"from_kb_paths()","text":"<pre><code>@classmethod\ndef from_kb_paths(\n cls,\n kb_paths: List[Path],\n provider_manager=None,\n) -> PlanningAgent\n</code></pre> <p>Factory method that creates an agent from one or more knowledge base file paths. Handles loading and merging knowledge graphs automatically.</p> <p>Parameters:</p> Parameter Type Default Description <code>kb_paths</code> <code>List[Path]</code> required Paths to <code>.db</code> or <code>.json</code> knowledge graph files, or directories to search <code>provider_manager</code> <code>ProviderManager</code> <code>None</code> LLM provider for agent operations <p>Returns: <code>PlanningAgent</code> -- configured agent with loaded knowledge base.</p> <pre><code>from pathlib import Path\nfrom video_processor.agent.agent_loop import PlanningAgent\nfrom video_processor.providers.manager import ProviderManager\n\nagent = PlanningAgent.from_kb_paths(\n kb_paths=[Path(\"results/knowledge_graph.db\")],\n provider_manager=ProviderManager(),\n)\n</code></pre>"},{"location":"api/agent/#execute","title":"execute()","text":"<pre><code>def execute(self, request: str) -> List[Artifact]\n</code></pre> <p>Execute a user request by selecting and running appropriate skills.</p> <p>Process:</p> <ol> <li>Build a context summary from the knowledge base statistics</li> <li>Format available skills with their descriptions</li> <li>Ask the LLM to select skills and parameters (or use keyword matching as fallback)</li> <li>Execute selected skills in order, accumulating artifacts</li> </ol> <p>Parameters:</p> Parameter Type Description <code>request</code> <code>str</code> Natural language request (e.g., \"Generate a project plan\") <p>Returns: <code>List[Artifact]</code> -- generated artifacts from skill execution.</p> <p>LLM mode: The LLM receives the knowledge base summary, available skills, and user request, then returns a JSON array of <code>{\"skill\": \"name\", \"params\": {}}</code> objects to execute.</p> <p>Keyword fallback: Without an LLM, skills are matched by splitting the skill name into words and checking if any appear in the request text.</p> <pre><code>artifacts = agent.execute(\"Create a PRD and task breakdown\")\nfor artifact in artifacts:\n print(f\"--- {artifact.name} ({artifact.artifact_type}) ---\")\n print(artifact.content[:500])\n</code></pre>"},{"location":"api/agent/#chat","title":"chat()","text":"<pre><code>def chat(self, message: str) -> str\n</code></pre> <p>Interactive chat mode. Maintains conversation history and provides contextual responses about the loaded knowledge base.</p> <p>Parameters:</p> Parameter Type Description <code>message</code> <code>str</code> User message <p>Returns: <code>str</code> -- assistant response.</p> <p>The chat mode provides the LLM with:</p> <ul> <li>Knowledge base statistics (entity counts, relationship counts)</li> <li>List of previously generated artifacts</li> <li>Full conversation history</li> <li>Available REPL commands (e.g., <code>/entities</code>, <code>/search</code>, <code>/plan</code>, <code>/export</code>)</li> </ul> <p>Requires a configured <code>provider_manager</code>. Returns a static error message if no LLM is available.</p> <pre><code>response = agent.chat(\"What technologies were discussed in the meetings?\")\nprint(response)\n\nresponse = agent.chat(\"Which of those have the most dependencies?\")\nprint(response)\n</code></pre>"},{"location":"api/agent/#agentcontext","title":"AgentContext","text":"<pre><code>from video_processor.agent.skills.base import AgentContext\n</code></pre> <p>Shared state dataclass passed to all skills during execution. Accumulates artifacts and conversation history across the agent session.</p> Field Type Default Description <code>knowledge_graph</code> <code>Any</code> <code>None</code> <code>KnowledgeGraph</code> instance <code>query_engine</code> <code>Any</code> <code>None</code> <code>GraphQueryEngine</code> instance for querying the KG <code>provider_manager</code> <code>Any</code> <code>None</code> <code>ProviderManager</code> instance for LLM calls <code>planning_entities</code> <code>List[Any]</code> <code>[]</code> Extracted <code>PlanningEntity</code> instances <code>user_requirements</code> <code>Dict[str, Any]</code> <code>{}</code> User-specified requirements and constraints <code>conversation_history</code> <code>List[Dict[str, str]]</code> <code>[]</code> Chat message history (<code>role</code>, <code>content</code> dicts) <code>artifacts</code> <code>List[Artifact]</code> <code>[]</code> Previously generated artifacts <code>config</code> <code>Dict[str, Any]</code> <code>{}</code> Additional configuration <pre><code>from video_processor.agent.skills.base import AgentContext\n\ncontext = AgentContext(\n knowledge_graph=kg,\n query_engine=engine,\n provider_manager=pm,\n config={\"output_format\": \"markdown\"},\n)\n</code></pre>"},{"location":"api/agent/#skill-abc","title":"Skill (ABC)","text":"<pre><code>from video_processor.agent.skills.base import Skill\n</code></pre> <p>Base class for agent skills. Each skill represents a discrete capability that produces an artifact from the agent context.</p> <p>Class attributes:</p> Attribute Type Description <code>name</code> <code>str</code> Skill identifier (e.g., <code>\"project_plan\"</code>, <code>\"prd\"</code>) <code>description</code> <code>str</code> Human-readable description shown to the LLM for skill selection"},{"location":"api/agent/#execute_1","title":"execute()","text":"<pre><code>@abstractmethod\ndef execute(self, context: AgentContext, **kwargs) -> Artifact\n</code></pre> <p>Execute this skill and return an artifact. Receives the shared agent context and any parameters selected by the LLM planner.</p>"},{"location":"api/agent/#can_execute","title":"can_execute()","text":"<pre><code>def can_execute(self, context: AgentContext) -> bool\n</code></pre> <p>Check if this skill can execute given the current context. The default implementation requires both <code>knowledge_graph</code> and <code>provider_manager</code> to be set. Override for skills with different requirements.</p> <p>Returns: <code>bool</code></p>"},{"location":"api/agent/#implementing-a-custom-skill","title":"Implementing a custom skill","text":"<pre><code>from video_processor.agent.skills.base import Skill, Artifact, AgentContext, register_skill\n\nclass SummarySkill(Skill):\n name = \"summary\"\n description = \"Generate a concise summary of the knowledge base\"\n\n def execute(self, context: AgentContext, **kwargs) -> Artifact:\n stats = context.query_engine.stats()\n prompt = f\"Summarize this knowledge base:\\n{stats.to_text()}\"\n content = context.provider_manager.chat(\n [{\"role\": \"user\", \"content\": prompt}]\n )\n return Artifact(\n name=\"Knowledge Base Summary\",\n content=content,\n artifact_type=\"document\",\n format=\"markdown\",\n )\n\n def can_execute(self, context: AgentContext) -> bool:\n return context.query_engine is not None and context.provider_manager is not None\n\n# Register the skill so the agent can discover it\nregister_skill(SummarySkill())\n</code></pre>"},{"location":"api/agent/#artifact","title":"Artifact","text":"<pre><code>from video_processor.agent.skills.base import Artifact\n</code></pre> <p>Dataclass representing the output of a skill execution.</p> Field Type Default Description <code>name</code> <code>str</code> required Human-readable artifact name <code>content</code> <code>str</code> required Generated content (Markdown, JSON, Mermaid, etc.) <code>artifact_type</code> <code>str</code> required Type: <code>\"project_plan\"</code>, <code>\"prd\"</code>, <code>\"roadmap\"</code>, <code>\"task_list\"</code>, <code>\"document\"</code>, <code>\"issues\"</code> <code>format</code> <code>str</code> <code>\"markdown\"</code> Content format: <code>\"markdown\"</code>, <code>\"json\"</code>, <code>\"mermaid\"</code> <code>metadata</code> <code>Dict[str, Any]</code> <code>{}</code> Additional metadata"},{"location":"api/agent/#skill-registry-functions","title":"Skill Registry Functions","text":""},{"location":"api/agent/#register_skill","title":"register_skill()","text":"<pre><code>def register_skill(skill: Skill) -> None\n</code></pre> <p>Register a skill instance in the global registry. Skills must be registered before the agent can discover and execute them.</p>"},{"location":"api/agent/#get_skill","title":"get_skill()","text":"<pre><code>def get_skill(name: str) -> Optional[Skill]\n</code></pre> <p>Look up a registered skill by name.</p> <p>Returns: <code>Optional[Skill]</code> -- the skill instance, or <code>None</code> if not found.</p>"},{"location":"api/agent/#list_skills","title":"list_skills()","text":"<pre><code>def list_skills() -> List[Skill]\n</code></pre> <p>Return all registered skill instances.</p>"},{"location":"api/agent/#kbcontext","title":"KBContext","text":"<pre><code>from video_processor.agent.kb_context import KBContext\n</code></pre> <p>Loads and merges multiple knowledge graph sources into a unified context for agent consumption. Supports both FalkorDB (<code>.db</code>) and JSON (<code>.json</code>) formats, and can auto-discover graphs in a directory tree.</p>"},{"location":"api/agent/#constructor_1","title":"Constructor","text":"<pre><code>def __init__(self)\n</code></pre> <p>Creates an empty context. Use <code>add_source()</code> to add knowledge graph paths, then <code>load()</code> to initialize.</p>"},{"location":"api/agent/#add_source","title":"add_source()","text":"<pre><code>def add_source(self, path) -> None\n</code></pre> <p>Add a knowledge graph source.</p> <p>Parameters:</p> Parameter Type Description <code>path</code> <code>str \\| Path</code> Path to a <code>.db</code> file, <code>.json</code> file, or directory to search for knowledge graphs <p>If <code>path</code> is a directory, it is searched recursively for knowledge graph files using <code>find_knowledge_graphs()</code>.</p> <p>Raises: <code>FileNotFoundError</code> if the path does not exist.</p>"},{"location":"api/agent/#load","title":"load()","text":"<pre><code>def load(self, provider_manager=None) -> KBContext\n</code></pre> <p>Load and merge all added sources into a single knowledge graph and query engine.</p> <p>Parameters:</p> Parameter Type Default Description <code>provider_manager</code> <code>ProviderManager</code> <code>None</code> LLM provider for the knowledge graph and query engine <p>Returns: <code>KBContext</code> -- self, for method chaining.</p>"},{"location":"api/agent/#properties","title":"Properties","text":"Property Type Description <code>knowledge_graph</code> <code>KnowledgeGraph</code> The merged knowledge graph (raises <code>RuntimeError</code> if not loaded) <code>query_engine</code> <code>GraphQueryEngine</code> Query engine for the merged graph (raises <code>RuntimeError</code> if not loaded) <code>sources</code> <code>List[Path]</code> List of resolved source paths"},{"location":"api/agent/#summary","title":"summary()","text":"<pre><code>def summary(self) -> str\n</code></pre> <p>Generate a brief text summary of the loaded knowledge base, including entity counts by type and relationship counts.</p> <p>Returns: <code>str</code> -- multi-line summary text.</p>"},{"location":"api/agent/#auto_discover","title":"auto_discover()","text":"<pre><code>@classmethod\ndef auto_discover(\n cls,\n start_dir: Optional[Path] = None,\n provider_manager=None,\n) -> KBContext\n</code></pre> <p>Factory method that creates a <code>KBContext</code> by auto-discovering knowledge graphs near <code>start_dir</code> (defaults to current directory).</p> <p>Returns: <code>KBContext</code> -- loaded context (may have zero sources if none found).</p>"},{"location":"api/agent/#usage-examples","title":"Usage examples","text":"<pre><code>from pathlib import Path\nfrom video_processor.agent.kb_context import KBContext\n\n# Manual source management\nkb = KBContext()\nkb.add_source(Path(\"project_a/knowledge_graph.db\"))\nkb.add_source(Path(\"project_b/results/\")) # searches directory\nkb.load(provider_manager=pm)\n\nprint(kb.summary())\n# Knowledge base: 3 source(s)\n# Entities: 142\n# Relationships: 89\n# Entity types:\n# technology: 45\n# person: 23\n# concept: 74\n\n# Auto-discover from current directory\nkb = KBContext.auto_discover()\n\n# Use with the agent\nfrom video_processor.agent.agent_loop import PlanningAgent\nfrom video_processor.agent.skills.base import AgentContext\n\ncontext = AgentContext(\n knowledge_graph=kb.knowledge_graph,\n query_engine=kb.query_engine,\n provider_manager=pm,\n)\nagent = PlanningAgent(context)\n</code></pre>"},{"location":"api/analyzers/","title":"Analyzers API Reference","text":""},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer","title":"<code>video_processor.analyzers.diagram_analyzer</code>","text":"<p>Diagram analysis using vision model classification and single-pass extraction.</p>"},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer.DiagramAnalyzer","title":"<code>DiagramAnalyzer</code>","text":"<p>Vision model-based diagram detection and analysis.</p> Source code in <code>video_processor/analyzers/diagram_analyzer.py</code> <pre><code>class DiagramAnalyzer:\n \"\"\"Vision model-based diagram detection and analysis.\"\"\"\n\n def __init__(\n self,\n provider_manager: Optional[ProviderManager] = None,\n confidence_threshold: float = 0.3,\n ):\n self.pm = provider_manager or ProviderManager()\n self.confidence_threshold = confidence_threshold\n\n def classify_frame(self, image_path: Union[str, Path]) -> dict:\n \"\"\"\n Classify a single frame using vision model.\n\n Returns dict with is_diagram, diagram_type, confidence, brief_description.\n \"\"\"\n image_bytes = _read_image_bytes(image_path)\n raw = self.pm.analyze_image(image_bytes, _CLASSIFY_PROMPT, max_tokens=512)\n result = _parse_json_response(raw)\n if result is None:\n return {\n \"is_diagram\": False,\n \"diagram_type\": \"unknown\",\n \"confidence\": 0.0,\n \"brief_description\": \"\",\n }\n return result\n\n def analyze_diagram_single_pass(self, image_path: Union[str, Path]) -> dict:\n \"\"\"\n Full single-pass diagram analysis \u2014 description, text, mermaid, chart data.\n\n Returns parsed dict or empty dict on failure.\n \"\"\"\n image_bytes = _read_image_bytes(image_path)\n raw = self.pm.analyze_image(image_bytes, _ANALYSIS_PROMPT, max_tokens=4096)\n result = _parse_json_response(raw)\n return result or {}\n\n def caption_frame(self, image_path: Union[str, Path]) -> str:\n \"\"\"Get a brief caption for a screengrab fallback.\"\"\"\n image_bytes = _read_image_bytes(image_path)\n return self.pm.analyze_image(image_bytes, _CAPTION_PROMPT, max_tokens=256)\n\n def process_frames(\n self,\n frame_paths: List[Union[str, Path]],\n diagrams_dir: Optional[Path] = None,\n captures_dir: Optional[Path] = None,\n ) -> Tuple[List[DiagramResult], List[ScreenCapture]]:\n \"\"\"\n Process a list of extracted frames: classify, analyze diagrams, screengrab fallback.\n\n Thresholds:\n - confidence >= 0.7 \u2192 full diagram analysis (story 3.2)\n - 0.3 <= confidence < 0.7 \u2192 screengrab fallback (story 3.3)\n - confidence < 0.3 \u2192 skip\n\n Returns (diagrams, screen_captures).\n \"\"\"\n diagrams: List[DiagramResult] = []\n captures: List[ScreenCapture] = []\n diagram_idx = 0\n capture_idx = 0\n\n for i, fp in enumerate(tqdm(frame_paths, desc=\"Analyzing frames\", unit=\"frame\")):\n fp = Path(fp)\n logger.info(f\"Classifying frame {i}/{len(frame_paths)}: {fp.name}\")\n\n try:\n classification = self.classify_frame(fp)\n except Exception as e:\n logger.warning(f\"Classification failed for frame {i}: {e}\")\n continue\n\n confidence = float(classification.get(\"confidence\", 0.0))\n\n if confidence < self.confidence_threshold:\n logger.debug(f\"Frame {i}: confidence {confidence:.2f} below threshold, skipping\")\n continue\n\n if confidence >= 0.7:\n # Full diagram analysis\n logger.info(\n f\"Frame {i}: diagram detected (confidence {confidence:.2f}), analyzing...\"\n )\n try:\n analysis = self.analyze_diagram_single_pass(fp)\n except Exception as e:\n logger.warning(\n f\"Diagram analysis failed for frame {i}: {e}, falling back to screengrab\"\n )\n analysis = {}\n\n if not analysis:\n # Analysis failed \u2014 fall back to screengrab\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n continue\n\n # Build DiagramResult\n dtype = analysis.get(\"diagram_type\", classification.get(\"diagram_type\", \"unknown\"))\n try:\n diagram_type = DiagramType(dtype)\n except ValueError:\n diagram_type = DiagramType.unknown\n\n # Normalize relationships: llava sometimes returns dicts instead of strings\n raw_rels = analysis.get(\"relationships\") or []\n relationships = []\n for rel in raw_rels:\n if isinstance(rel, str):\n relationships.append(rel)\n elif isinstance(rel, dict):\n src = rel.get(\"source\", rel.get(\"from\", \"?\"))\n dst = rel.get(\"destination\", rel.get(\"to\", \"?\"))\n label = rel.get(\"label\", rel.get(\"relationship\", \"\"))\n relationships.append(\n f\"{src} -> {dst}: {label}\" if label else f\"{src} -> {dst}\"\n )\n else:\n relationships.append(str(rel))\n\n # Normalize elements: llava may return dicts or nested lists\n raw_elements = analysis.get(\"elements\") or []\n elements = []\n for elem in raw_elements:\n if isinstance(elem, str):\n elements.append(elem)\n elif isinstance(elem, dict):\n name = elem.get(\"name\", elem.get(\"element\", \"\"))\n etype = elem.get(\"type\", elem.get(\"element_type\", \"\"))\n if name and etype:\n elements.append(f\"{etype}: {name}\")\n elif name:\n elements.append(name)\n else:\n elements.append(json.dumps(elem))\n elif isinstance(elem, list):\n elements.extend(str(e) for e in elem)\n else:\n elements.append(str(elem))\n\n # Normalize text_content: llava may return dict instead of string\n raw_text = analysis.get(\"text_content\")\n if isinstance(raw_text, dict):\n parts = []\n for k, v in raw_text.items():\n if isinstance(v, list):\n parts.append(f\"{k}: {', '.join(str(x) for x in v)}\")\n else:\n parts.append(f\"{k}: {v}\")\n text_content = \"\\n\".join(parts)\n elif isinstance(raw_text, list):\n text_content = \"\\n\".join(str(x) for x in raw_text)\n else:\n text_content = raw_text\n\n try:\n dr = DiagramResult(\n frame_index=i,\n diagram_type=diagram_type,\n confidence=confidence,\n description=analysis.get(\"description\"),\n text_content=text_content,\n elements=elements,\n relationships=relationships,\n mermaid=analysis.get(\"mermaid\"),\n chart_data=analysis.get(\"chart_data\"),\n )\n except Exception as e:\n logger.warning(\n f\"DiagramResult validation failed for frame {i}: {e}, \"\n \"falling back to screengrab\"\n )\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n continue\n\n # Save outputs (story 3.4)\n if diagrams_dir:\n diagrams_dir.mkdir(parents=True, exist_ok=True)\n prefix = f\"diagram_{diagram_idx}\"\n\n # Original frame\n img_dest = diagrams_dir / f\"{prefix}.jpg\"\n shutil.copy2(fp, img_dest)\n dr.image_path = f\"diagrams/{prefix}.jpg\"\n\n # Mermaid source\n if dr.mermaid:\n mermaid_dest = diagrams_dir / f\"{prefix}.mermaid\"\n mermaid_dest.write_text(dr.mermaid)\n dr.mermaid_path = f\"diagrams/{prefix}.mermaid\"\n\n # Analysis JSON\n json_dest = diagrams_dir / f\"{prefix}.json\"\n json_dest.write_text(dr.model_dump_json(indent=2))\n\n diagrams.append(dr)\n diagram_idx += 1\n\n else:\n # Screengrab fallback (0.3 <= confidence < 0.7)\n logger.info(\n f\"Frame {i}: uncertain (confidence {confidence:.2f}), saving as screengrab\"\n )\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n\n logger.info(\n f\"Diagram processing complete: {len(diagrams)} diagrams, {len(captures)} screengrabs\"\n )\n return diagrams, captures\n\n def _save_screengrab(\n self,\n frame_path: Path,\n frame_index: int,\n capture_index: int,\n captures_dir: Optional[Path],\n confidence: float,\n ) -> ScreenCapture:\n \"\"\"Save a frame as a captioned screengrab.\"\"\"\n caption = \"\"\n try:\n caption = self.caption_frame(frame_path)\n except Exception as e:\n logger.warning(f\"Caption failed for frame {frame_index}: {e}\")\n\n sc = ScreenCapture(\n frame_index=frame_index,\n caption=caption,\n confidence=confidence,\n )\n\n if captures_dir:\n captures_dir.mkdir(parents=True, exist_ok=True)\n prefix = f\"capture_{capture_index}\"\n img_dest = captures_dir / f\"{prefix}.jpg\"\n shutil.copy2(frame_path, img_dest)\n sc.image_path = f\"captures/{prefix}.jpg\"\n\n json_dest = captures_dir / f\"{prefix}.json\"\n json_dest.write_text(sc.model_dump_json(indent=2))\n\n return sc\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer.DiagramAnalyzer.analyze_diagram_single_pass","title":"<code>analyze_diagram_single_pass(image_path)</code>","text":"<p>Full single-pass diagram analysis \u2014 description, text, mermaid, chart data.</p> <p>Returns parsed dict or empty dict on failure.</p> Source code in <code>video_processor/analyzers/diagram_analyzer.py</code> <pre><code>def analyze_diagram_single_pass(self, image_path: Union[str, Path]) -> dict:\n \"\"\"\n Full single-pass diagram analysis \u2014 description, text, mermaid, chart data.\n\n Returns parsed dict or empty dict on failure.\n \"\"\"\n image_bytes = _read_image_bytes(image_path)\n raw = self.pm.analyze_image(image_bytes, _ANALYSIS_PROMPT, max_tokens=4096)\n result = _parse_json_response(raw)\n return result or {}\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer.DiagramAnalyzer.caption_frame","title":"<code>caption_frame(image_path)</code>","text":"<p>Get a brief caption for a screengrab fallback.</p> Source code in <code>video_processor/analyzers/diagram_analyzer.py</code> <pre><code>def caption_frame(self, image_path: Union[str, Path]) -> str:\n \"\"\"Get a brief caption for a screengrab fallback.\"\"\"\n image_bytes = _read_image_bytes(image_path)\n return self.pm.analyze_image(image_bytes, _CAPTION_PROMPT, max_tokens=256)\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer.DiagramAnalyzer.classify_frame","title":"<code>classify_frame(image_path)</code>","text":"<p>Classify a single frame using vision model.</p> <p>Returns dict with is_diagram, diagram_type, confidence, brief_description.</p> Source code in <code>video_processor/analyzers/diagram_analyzer.py</code> <pre><code>def classify_frame(self, image_path: Union[str, Path]) -> dict:\n \"\"\"\n Classify a single frame using vision model.\n\n Returns dict with is_diagram, diagram_type, confidence, brief_description.\n \"\"\"\n image_bytes = _read_image_bytes(image_path)\n raw = self.pm.analyze_image(image_bytes, _CLASSIFY_PROMPT, max_tokens=512)\n result = _parse_json_response(raw)\n if result is None:\n return {\n \"is_diagram\": False,\n \"diagram_type\": \"unknown\",\n \"confidence\": 0.0,\n \"brief_description\": \"\",\n }\n return result\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.diagram_analyzer.DiagramAnalyzer.process_frames","title":"<code>process_frames(frame_paths, diagrams_dir=None, captures_dir=None)</code>","text":"<p>Process a list of extracted frames: classify, analyze diagrams, screengrab fallback.</p> Thresholds <ul> <li>confidence >= 0.7 \u2192 full diagram analysis (story 3.2)</li> <li>0.3 <= confidence < 0.7 \u2192 screengrab fallback (story 3.3)</li> <li>confidence < 0.3 \u2192 skip</li> </ul> <p>Returns (diagrams, screen_captures).</p> Source code in <code>video_processor/analyzers/diagram_analyzer.py</code> <pre><code>def process_frames(\n self,\n frame_paths: List[Union[str, Path]],\n diagrams_dir: Optional[Path] = None,\n captures_dir: Optional[Path] = None,\n) -> Tuple[List[DiagramResult], List[ScreenCapture]]:\n \"\"\"\n Process a list of extracted frames: classify, analyze diagrams, screengrab fallback.\n\n Thresholds:\n - confidence >= 0.7 \u2192 full diagram analysis (story 3.2)\n - 0.3 <= confidence < 0.7 \u2192 screengrab fallback (story 3.3)\n - confidence < 0.3 \u2192 skip\n\n Returns (diagrams, screen_captures).\n \"\"\"\n diagrams: List[DiagramResult] = []\n captures: List[ScreenCapture] = []\n diagram_idx = 0\n capture_idx = 0\n\n for i, fp in enumerate(tqdm(frame_paths, desc=\"Analyzing frames\", unit=\"frame\")):\n fp = Path(fp)\n logger.info(f\"Classifying frame {i}/{len(frame_paths)}: {fp.name}\")\n\n try:\n classification = self.classify_frame(fp)\n except Exception as e:\n logger.warning(f\"Classification failed for frame {i}: {e}\")\n continue\n\n confidence = float(classification.get(\"confidence\", 0.0))\n\n if confidence < self.confidence_threshold:\n logger.debug(f\"Frame {i}: confidence {confidence:.2f} below threshold, skipping\")\n continue\n\n if confidence >= 0.7:\n # Full diagram analysis\n logger.info(\n f\"Frame {i}: diagram detected (confidence {confidence:.2f}), analyzing...\"\n )\n try:\n analysis = self.analyze_diagram_single_pass(fp)\n except Exception as e:\n logger.warning(\n f\"Diagram analysis failed for frame {i}: {e}, falling back to screengrab\"\n )\n analysis = {}\n\n if not analysis:\n # Analysis failed \u2014 fall back to screengrab\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n continue\n\n # Build DiagramResult\n dtype = analysis.get(\"diagram_type\", classification.get(\"diagram_type\", \"unknown\"))\n try:\n diagram_type = DiagramType(dtype)\n except ValueError:\n diagram_type = DiagramType.unknown\n\n # Normalize relationships: llava sometimes returns dicts instead of strings\n raw_rels = analysis.get(\"relationships\") or []\n relationships = []\n for rel in raw_rels:\n if isinstance(rel, str):\n relationships.append(rel)\n elif isinstance(rel, dict):\n src = rel.get(\"source\", rel.get(\"from\", \"?\"))\n dst = rel.get(\"destination\", rel.get(\"to\", \"?\"))\n label = rel.get(\"label\", rel.get(\"relationship\", \"\"))\n relationships.append(\n f\"{src} -> {dst}: {label}\" if label else f\"{src} -> {dst}\"\n )\n else:\n relationships.append(str(rel))\n\n # Normalize elements: llava may return dicts or nested lists\n raw_elements = analysis.get(\"elements\") or []\n elements = []\n for elem in raw_elements:\n if isinstance(elem, str):\n elements.append(elem)\n elif isinstance(elem, dict):\n name = elem.get(\"name\", elem.get(\"element\", \"\"))\n etype = elem.get(\"type\", elem.get(\"element_type\", \"\"))\n if name and etype:\n elements.append(f\"{etype}: {name}\")\n elif name:\n elements.append(name)\n else:\n elements.append(json.dumps(elem))\n elif isinstance(elem, list):\n elements.extend(str(e) for e in elem)\n else:\n elements.append(str(elem))\n\n # Normalize text_content: llava may return dict instead of string\n raw_text = analysis.get(\"text_content\")\n if isinstance(raw_text, dict):\n parts = []\n for k, v in raw_text.items():\n if isinstance(v, list):\n parts.append(f\"{k}: {', '.join(str(x) for x in v)}\")\n else:\n parts.append(f\"{k}: {v}\")\n text_content = \"\\n\".join(parts)\n elif isinstance(raw_text, list):\n text_content = \"\\n\".join(str(x) for x in raw_text)\n else:\n text_content = raw_text\n\n try:\n dr = DiagramResult(\n frame_index=i,\n diagram_type=diagram_type,\n confidence=confidence,\n description=analysis.get(\"description\"),\n text_content=text_content,\n elements=elements,\n relationships=relationships,\n mermaid=analysis.get(\"mermaid\"),\n chart_data=analysis.get(\"chart_data\"),\n )\n except Exception as e:\n logger.warning(\n f\"DiagramResult validation failed for frame {i}: {e}, \"\n \"falling back to screengrab\"\n )\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n continue\n\n # Save outputs (story 3.4)\n if diagrams_dir:\n diagrams_dir.mkdir(parents=True, exist_ok=True)\n prefix = f\"diagram_{diagram_idx}\"\n\n # Original frame\n img_dest = diagrams_dir / f\"{prefix}.jpg\"\n shutil.copy2(fp, img_dest)\n dr.image_path = f\"diagrams/{prefix}.jpg\"\n\n # Mermaid source\n if dr.mermaid:\n mermaid_dest = diagrams_dir / f\"{prefix}.mermaid\"\n mermaid_dest.write_text(dr.mermaid)\n dr.mermaid_path = f\"diagrams/{prefix}.mermaid\"\n\n # Analysis JSON\n json_dest = diagrams_dir / f\"{prefix}.json\"\n json_dest.write_text(dr.model_dump_json(indent=2))\n\n diagrams.append(dr)\n diagram_idx += 1\n\n else:\n # Screengrab fallback (0.3 <= confidence < 0.7)\n logger.info(\n f\"Frame {i}: uncertain (confidence {confidence:.2f}), saving as screengrab\"\n )\n capture = self._save_screengrab(fp, i, capture_idx, captures_dir, confidence)\n captures.append(capture)\n capture_idx += 1\n\n logger.info(\n f\"Diagram processing complete: {len(diagrams)} diagrams, {len(captures)} screengrabs\"\n )\n return diagrams, captures\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.content_analyzer","title":"<code>video_processor.analyzers.content_analyzer</code>","text":"<p>Content cross-referencing between transcript and diagram entities.</p>"},{"location":"api/analyzers/#video_processor.analyzers.content_analyzer.ContentAnalyzer","title":"<code>ContentAnalyzer</code>","text":"<p>Cross-references transcript and diagram entities for richer knowledge.</p> Source code in <code>video_processor/analyzers/content_analyzer.py</code> <pre><code>class ContentAnalyzer:\n \"\"\"Cross-references transcript and diagram entities for richer knowledge.\"\"\"\n\n def __init__(self, provider_manager: Optional[ProviderManager] = None):\n self.pm = provider_manager\n\n def cross_reference(\n self,\n transcript_entities: List[Entity],\n diagram_entities: List[Entity],\n ) -> List[Entity]:\n \"\"\"\n Merge entities from transcripts and diagrams.\n\n Merges by exact name overlap first, then uses LLM for fuzzy matching\n of remaining entities. Adds source attribution.\n \"\"\"\n merged: dict[str, Entity] = {}\n\n # Index transcript entities\n for e in transcript_entities:\n key = e.name.lower()\n merged[key] = Entity(\n name=e.name,\n type=e.type,\n descriptions=list(e.descriptions),\n source=\"transcript\",\n occurrences=list(e.occurrences),\n )\n\n # Merge diagram entities\n for e in diagram_entities:\n key = e.name.lower()\n if key in merged:\n existing = merged[key]\n existing.source = \"both\"\n existing.descriptions = list(set(existing.descriptions + e.descriptions))\n existing.occurrences.extend(e.occurrences)\n else:\n merged[key] = Entity(\n name=e.name,\n type=e.type,\n descriptions=list(e.descriptions),\n source=\"diagram\",\n occurrences=list(e.occurrences),\n )\n\n # LLM fuzzy matching for unmatched entities\n if self.pm:\n unmatched_t = [\n e\n for e in transcript_entities\n if e.name.lower() not in {d.name.lower() for d in diagram_entities}\n ]\n unmatched_d = [\n e\n for e in diagram_entities\n if e.name.lower() not in {t.name.lower() for t in transcript_entities}\n ]\n\n if unmatched_t and unmatched_d:\n matches = self._fuzzy_match(unmatched_t, unmatched_d)\n for t_name, d_name in matches:\n t_key = t_name.lower()\n d_key = d_name.lower()\n if t_key in merged and d_key in merged:\n t_entity = merged[t_key]\n d_entity = merged.pop(d_key)\n t_entity.source = \"both\"\n t_entity.descriptions = list(\n set(t_entity.descriptions + d_entity.descriptions)\n )\n t_entity.occurrences.extend(d_entity.occurrences)\n\n return list(merged.values())\n\n def _fuzzy_match(\n self,\n transcript_entities: List[Entity],\n diagram_entities: List[Entity],\n ) -> List[tuple[str, str]]:\n \"\"\"Use LLM to fuzzy-match entity names across sources.\"\"\"\n if not self.pm:\n return []\n\n t_names = [e.name for e in transcript_entities]\n d_names = [e.name for e in diagram_entities]\n\n prompt = (\n \"Match entities that refer to the same thing across these two lists.\\n\\n\"\n f\"Transcript entities: {t_names}\\n\"\n f\"Diagram entities: {d_names}\\n\\n\"\n \"Return a JSON array of matched pairs:\\n\"\n '[{\"transcript\": \"name from list 1\", \"diagram\": \"name from list 2\"}]\\n\\n'\n \"Only include confident matches. Return empty array if no matches.\\n\"\n \"Return ONLY the JSON array.\"\n )\n\n try:\n raw = self.pm.chat([{\"role\": \"user\", \"content\": prompt}], temperature=0.2)\n parsed = parse_json_from_response(raw)\n if isinstance(parsed, list):\n return [\n (item[\"transcript\"], item[\"diagram\"])\n for item in parsed\n if isinstance(item, dict) and \"transcript\" in item and \"diagram\" in item\n ]\n except Exception as e:\n logger.warning(f\"Fuzzy matching failed: {e}\")\n\n return []\n\n def enrich_key_points(\n self,\n key_points: List[KeyPoint],\n diagrams: list,\n transcript_text: str,\n ) -> List[KeyPoint]:\n \"\"\"\n Link key points to relevant diagrams by entity overlap and temporal proximity.\n \"\"\"\n if not diagrams:\n return key_points\n\n # Build diagram entity index\n diagram_entities: dict[int, set[str]] = {}\n for i, d in enumerate(diagrams):\n elements = d.get(\"elements\", []) if isinstance(d, dict) else getattr(d, \"elements\", [])\n text = (\n d.get(\"text_content\", \"\") if isinstance(d, dict) else getattr(d, \"text_content\", \"\")\n )\n entities = set(str(e).lower() for e in elements)\n if text:\n entities.update(word.lower() for word in text.split() if len(word) > 3)\n diagram_entities[i] = entities\n\n # Match key points to diagrams\n for kp in key_points:\n kp_words = set(kp.point.lower().split())\n if kp.details:\n kp_words.update(kp.details.lower().split())\n\n related = []\n for idx, d_entities in diagram_entities.items():\n overlap = kp_words & d_entities\n if len(overlap) >= 2:\n related.append(idx)\n\n if related:\n kp.related_diagrams = related\n\n return key_points\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.content_analyzer.ContentAnalyzer.cross_reference","title":"<code>cross_reference(transcript_entities, diagram_entities)</code>","text":"<p>Merge entities from transcripts and diagrams.</p> <p>Merges by exact name overlap first, then uses LLM for fuzzy matching of remaining entities. Adds source attribution.</p> Source code in <code>video_processor/analyzers/content_analyzer.py</code> <pre><code>def cross_reference(\n self,\n transcript_entities: List[Entity],\n diagram_entities: List[Entity],\n) -> List[Entity]:\n \"\"\"\n Merge entities from transcripts and diagrams.\n\n Merges by exact name overlap first, then uses LLM for fuzzy matching\n of remaining entities. Adds source attribution.\n \"\"\"\n merged: dict[str, Entity] = {}\n\n # Index transcript entities\n for e in transcript_entities:\n key = e.name.lower()\n merged[key] = Entity(\n name=e.name,\n type=e.type,\n descriptions=list(e.descriptions),\n source=\"transcript\",\n occurrences=list(e.occurrences),\n )\n\n # Merge diagram entities\n for e in diagram_entities:\n key = e.name.lower()\n if key in merged:\n existing = merged[key]\n existing.source = \"both\"\n existing.descriptions = list(set(existing.descriptions + e.descriptions))\n existing.occurrences.extend(e.occurrences)\n else:\n merged[key] = Entity(\n name=e.name,\n type=e.type,\n descriptions=list(e.descriptions),\n source=\"diagram\",\n occurrences=list(e.occurrences),\n )\n\n # LLM fuzzy matching for unmatched entities\n if self.pm:\n unmatched_t = [\n e\n for e in transcript_entities\n if e.name.lower() not in {d.name.lower() for d in diagram_entities}\n ]\n unmatched_d = [\n e\n for e in diagram_entities\n if e.name.lower() not in {t.name.lower() for t in transcript_entities}\n ]\n\n if unmatched_t and unmatched_d:\n matches = self._fuzzy_match(unmatched_t, unmatched_d)\n for t_name, d_name in matches:\n t_key = t_name.lower()\n d_key = d_name.lower()\n if t_key in merged and d_key in merged:\n t_entity = merged[t_key]\n d_entity = merged.pop(d_key)\n t_entity.source = \"both\"\n t_entity.descriptions = list(\n set(t_entity.descriptions + d_entity.descriptions)\n )\n t_entity.occurrences.extend(d_entity.occurrences)\n\n return list(merged.values())\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.content_analyzer.ContentAnalyzer.enrich_key_points","title":"<code>enrich_key_points(key_points, diagrams, transcript_text)</code>","text":"<p>Link key points to relevant diagrams by entity overlap and temporal proximity.</p> Source code in <code>video_processor/analyzers/content_analyzer.py</code> <pre><code>def enrich_key_points(\n self,\n key_points: List[KeyPoint],\n diagrams: list,\n transcript_text: str,\n) -> List[KeyPoint]:\n \"\"\"\n Link key points to relevant diagrams by entity overlap and temporal proximity.\n \"\"\"\n if not diagrams:\n return key_points\n\n # Build diagram entity index\n diagram_entities: dict[int, set[str]] = {}\n for i, d in enumerate(diagrams):\n elements = d.get(\"elements\", []) if isinstance(d, dict) else getattr(d, \"elements\", [])\n text = (\n d.get(\"text_content\", \"\") if isinstance(d, dict) else getattr(d, \"text_content\", \"\")\n )\n entities = set(str(e).lower() for e in elements)\n if text:\n entities.update(word.lower() for word in text.split() if len(word) > 3)\n diagram_entities[i] = entities\n\n # Match key points to diagrams\n for kp in key_points:\n kp_words = set(kp.point.lower().split())\n if kp.details:\n kp_words.update(kp.details.lower().split())\n\n related = []\n for idx, d_entities in diagram_entities.items():\n overlap = kp_words & d_entities\n if len(overlap) >= 2:\n related.append(idx)\n\n if related:\n kp.related_diagrams = related\n\n return key_points\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.action_detector","title":"<code>video_processor.analyzers.action_detector</code>","text":"<p>Enhanced action item detection from transcripts and diagrams.</p>"},{"location":"api/analyzers/#video_processor.analyzers.action_detector.ActionDetector","title":"<code>ActionDetector</code>","text":"<p>Detects action items from transcripts using heuristics and LLM.</p> Source code in <code>video_processor/analyzers/action_detector.py</code> <pre><code>class ActionDetector:\n \"\"\"Detects action items from transcripts using heuristics and LLM.\"\"\"\n\n def __init__(self, provider_manager: Optional[ProviderManager] = None):\n self.pm = provider_manager\n\n def detect_from_transcript(\n self,\n text: str,\n segments: Optional[List[TranscriptSegment]] = None,\n ) -> List[ActionItem]:\n \"\"\"\n Detect action items from transcript text.\n\n Uses LLM extraction when available, falls back to pattern matching.\n Segments are used to attach timestamps.\n \"\"\"\n if self.pm:\n items = self._llm_extract(text)\n else:\n items = self._pattern_extract(text)\n\n # Attach timestamps from segments if available\n if segments and items:\n self._attach_timestamps(items, segments)\n\n return items\n\n def detect_from_diagrams(\n self,\n diagrams: list,\n ) -> List[ActionItem]:\n \"\"\"\n Extract action items mentioned in diagram text content.\n\n Looks for action-oriented language in diagram text/elements.\n \"\"\"\n items: List[ActionItem] = []\n\n for diagram in diagrams:\n text = \"\"\n if isinstance(diagram, dict):\n text = diagram.get(\"text_content\", \"\") or \"\"\n elements = diagram.get(\"elements\", [])\n else:\n text = getattr(diagram, \"text_content\", \"\") or \"\"\n elements = getattr(diagram, \"elements\", [])\n\n combined = text + \" \" + \" \".join(str(e) for e in elements)\n if not combined.strip():\n continue\n\n if self.pm:\n diagram_items = self._llm_extract(combined)\n else:\n diagram_items = self._pattern_extract(combined)\n\n for item in diagram_items:\n item.source = \"diagram\"\n items.extend(diagram_items)\n\n return items\n\n def merge_action_items(\n self,\n transcript_items: List[ActionItem],\n diagram_items: List[ActionItem],\n ) -> List[ActionItem]:\n \"\"\"\n Merge action items from transcript and diagram sources.\n\n Deduplicates by checking for similar action text.\n \"\"\"\n merged: List[ActionItem] = list(transcript_items)\n existing_actions = {a.action.lower().strip() for a in merged}\n\n for item in diagram_items:\n normalized = item.action.lower().strip()\n if normalized not in existing_actions:\n merged.append(item)\n existing_actions.add(normalized)\n\n return merged\n\n def _llm_extract(self, text: str) -> List[ActionItem]:\n \"\"\"Extract action items using LLM.\"\"\"\n if not self.pm:\n return []\n\n prompt = (\n \"Extract all action items, tasks, and commitments \"\n \"from the following text.\\n\\n\"\n f\"TEXT:\\n{text[:8000]}\\n\\n\"\n \"Return a JSON array:\\n\"\n '[{\"action\": \"...\", \"assignee\": \"...\", \"deadline\": \"...\", '\n '\"priority\": \"...\", \"context\": \"...\"}]\\n\\n'\n \"Only include clear, actionable items. \"\n \"Set fields to null if not mentioned.\\n\"\n \"Return ONLY the JSON array.\"\n )\n\n try:\n raw = self.pm.chat(\n [{\"role\": \"user\", \"content\": prompt}],\n temperature=0.3,\n )\n parsed = parse_json_from_response(raw)\n if isinstance(parsed, list):\n return [\n ActionItem(\n action=item.get(\"action\", \"\"),\n assignee=item.get(\"assignee\"),\n deadline=item.get(\"deadline\"),\n priority=item.get(\"priority\"),\n context=item.get(\"context\"),\n source=\"transcript\",\n )\n for item in parsed\n if isinstance(item, dict) and item.get(\"action\")\n ]\n except Exception as e:\n logger.warning(f\"LLM action extraction failed: {e}\")\n\n return []\n\n def _pattern_extract(self, text: str) -> List[ActionItem]:\n \"\"\"Extract action items using regex pattern matching.\"\"\"\n items: List[ActionItem] = []\n sentences = re.split(r\"[.!?]\\s+\", text)\n\n for sentence in sentences:\n sentence = sentence.strip()\n if not sentence or len(sentence) < 10:\n continue\n\n for pattern in _ACTION_PATTERNS:\n if pattern.search(sentence):\n items.append(\n ActionItem(\n action=sentence,\n source=\"transcript\",\n )\n )\n break # One match per sentence is enough\n\n return items\n\n def _attach_timestamps(\n self,\n items: List[ActionItem],\n segments: List[TranscriptSegment],\n ) -> None:\n \"\"\"Attach timestamps to action items by finding matching segments.\"\"\"\n for item in items:\n action_lower = item.action.lower()\n best_overlap = 0\n best_segment = None\n\n for seg in segments:\n seg_lower = seg.text.lower()\n # Check word overlap\n action_words = set(action_lower.split())\n seg_words = set(seg_lower.split())\n overlap = len(action_words & seg_words)\n\n if overlap > best_overlap:\n best_overlap = overlap\n best_segment = seg\n\n if best_segment and best_overlap >= 3:\n if not item.context:\n item.context = f\"at {best_segment.start:.0f}s\"\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.action_detector.ActionDetector.detect_from_diagrams","title":"<code>detect_from_diagrams(diagrams)</code>","text":"<p>Extract action items mentioned in diagram text content.</p> <p>Looks for action-oriented language in diagram text/elements.</p> Source code in <code>video_processor/analyzers/action_detector.py</code> <pre><code>def detect_from_diagrams(\n self,\n diagrams: list,\n) -> List[ActionItem]:\n \"\"\"\n Extract action items mentioned in diagram text content.\n\n Looks for action-oriented language in diagram text/elements.\n \"\"\"\n items: List[ActionItem] = []\n\n for diagram in diagrams:\n text = \"\"\n if isinstance(diagram, dict):\n text = diagram.get(\"text_content\", \"\") or \"\"\n elements = diagram.get(\"elements\", [])\n else:\n text = getattr(diagram, \"text_content\", \"\") or \"\"\n elements = getattr(diagram, \"elements\", [])\n\n combined = text + \" \" + \" \".join(str(e) for e in elements)\n if not combined.strip():\n continue\n\n if self.pm:\n diagram_items = self._llm_extract(combined)\n else:\n diagram_items = self._pattern_extract(combined)\n\n for item in diagram_items:\n item.source = \"diagram\"\n items.extend(diagram_items)\n\n return items\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.action_detector.ActionDetector.detect_from_transcript","title":"<code>detect_from_transcript(text, segments=None)</code>","text":"<p>Detect action items from transcript text.</p> <p>Uses LLM extraction when available, falls back to pattern matching. Segments are used to attach timestamps.</p> Source code in <code>video_processor/analyzers/action_detector.py</code> <pre><code>def detect_from_transcript(\n self,\n text: str,\n segments: Optional[List[TranscriptSegment]] = None,\n) -> List[ActionItem]:\n \"\"\"\n Detect action items from transcript text.\n\n Uses LLM extraction when available, falls back to pattern matching.\n Segments are used to attach timestamps.\n \"\"\"\n if self.pm:\n items = self._llm_extract(text)\n else:\n items = self._pattern_extract(text)\n\n # Attach timestamps from segments if available\n if segments and items:\n self._attach_timestamps(items, segments)\n\n return items\n</code></pre>"},{"location":"api/analyzers/#video_processor.analyzers.action_detector.ActionDetector.merge_action_items","title":"<code>merge_action_items(transcript_items, diagram_items)</code>","text":"<p>Merge action items from transcript and diagram sources.</p> <p>Deduplicates by checking for similar action text.</p> Source code in <code>video_processor/analyzers/action_detector.py</code> <pre><code>def merge_action_items(\n self,\n transcript_items: List[ActionItem],\n diagram_items: List[ActionItem],\n) -> List[ActionItem]:\n \"\"\"\n Merge action items from transcript and diagram sources.\n\n Deduplicates by checking for similar action text.\n \"\"\"\n merged: List[ActionItem] = list(transcript_items)\n existing_actions = {a.action.lower().strip() for a in merged}\n\n for item in diagram_items:\n normalized = item.action.lower().strip()\n if normalized not in existing_actions:\n merged.append(item)\n existing_actions.add(normalized)\n\n return merged\n</code></pre>"},{"location":"api/analyzers/#overview","title":"Overview","text":"<p>The analyzers module contains the core content extraction logic for PlanOpticon. These analyzers process video frames and transcripts to extract structured knowledge: diagrams, key points, action items, and cross-referenced entities.</p> <p>All analyzers accept an optional <code>ProviderManager</code> instance. When provided, they use LLM capabilities for richer extraction. Without one, they fall back to heuristic/pattern-based methods where possible.</p>"},{"location":"api/analyzers/#diagramanalyzer","title":"DiagramAnalyzer","text":"<pre><code>from video_processor.analyzers.diagram_analyzer import DiagramAnalyzer\n</code></pre> <p>Vision model-based diagram detection and analysis. Classifies video frames as diagrams, slides, screenshots, or other content, then performs full extraction on high-confidence frames.</p>"},{"location":"api/analyzers/#constructor","title":"Constructor","text":"<pre><code>def __init__(\n self,\n provider_manager: Optional[ProviderManager] = None,\n confidence_threshold: float = 0.3,\n)\n</code></pre> Parameter Type Default Description <code>provider_manager</code> <code>Optional[ProviderManager]</code> <code>None</code> LLM provider (creates a default if not provided) <code>confidence_threshold</code> <code>float</code> <code>0.3</code> Minimum confidence to process a frame at all"},{"location":"api/analyzers/#classify_frame","title":"classify_frame()","text":"<pre><code>def classify_frame(self, image_path: Union[str, Path]) -> dict\n</code></pre> <p>Classify a single frame using a vision model. Determines whether the frame contains a diagram, slide, or other visual content worth extracting.</p> <p>Parameters:</p> Parameter Type Description <code>image_path</code> <code>Union[str, Path]</code> Path to the frame image file <p>Returns: <code>dict</code> with the following keys:</p> Key Type Description <code>is_diagram</code> <code>bool</code> Whether the frame contains extractable content <code>diagram_type</code> <code>str</code> One of: <code>flowchart</code>, <code>sequence</code>, <code>architecture</code>, <code>whiteboard</code>, <code>chart</code>, <code>table</code>, <code>slide</code>, <code>screenshot</code>, <code>unknown</code> <code>confidence</code> <code>float</code> Detection confidence from 0.0 to 1.0 <code>content_type</code> <code>str</code> Content category: <code>slide</code>, <code>diagram</code>, <code>document</code>, <code>screen_share</code>, <code>whiteboard</code>, <code>chart</code>, <code>person</code>, <code>other</code> <code>brief_description</code> <code>str</code> One-sentence description of the frame content <p>Important: Frames showing people, webcam feeds, or video conference participant views return <code>confidence: 0.0</code>. The classifier is tuned to detect only shared/presented content.</p> <pre><code>analyzer = DiagramAnalyzer()\nresult = analyzer.classify_frame(\"/path/to/frame_042.jpg\")\nif result[\"confidence\"] >= 0.7:\n print(f\"Diagram detected: {result['diagram_type']}\")\n</code></pre>"},{"location":"api/analyzers/#analyze_diagram_single_pass","title":"analyze_diagram_single_pass()","text":"<pre><code>def analyze_diagram_single_pass(self, image_path: Union[str, Path]) -> dict\n</code></pre> <p>Full single-pass diagram analysis. Extracts description, text content, elements, relationships, Mermaid syntax, and chart data in a single LLM call.</p> <p>Returns: <code>dict</code> with the following keys:</p> Key Type Description <code>diagram_type</code> <code>str</code> Diagram classification <code>description</code> <code>str</code> Detailed description of the visual content <code>text_content</code> <code>str</code> All visible text, preserving structure <code>elements</code> <code>list[str]</code> Identified elements/components <code>relationships</code> <code>list[str]</code> Relationships in <code>\"A -> B: label\"</code> format <code>mermaid</code> <code>str</code> Valid Mermaid diagram syntax <code>chart_data</code> <code>dict \\| None</code> Chart data with <code>labels</code>, <code>values</code>, <code>chart_type</code> (only for data charts) <p>Returns an empty <code>dict</code> on failure.</p>"},{"location":"api/analyzers/#caption_frame","title":"caption_frame()","text":"<pre><code>def caption_frame(self, image_path: Union[str, Path]) -> str\n</code></pre> <p>Get a brief 1-2 sentence caption for a frame. Used as a fallback when full diagram analysis is not warranted.</p> <p>Returns: <code>str</code> -- a brief description of the frame content.</p>"},{"location":"api/analyzers/#process_frames","title":"process_frames()","text":"<pre><code>def process_frames(\n self,\n frame_paths: List[Union[str, Path]],\n diagrams_dir: Optional[Path] = None,\n captures_dir: Optional[Path] = None,\n) -> Tuple[List[DiagramResult], List[ScreenCapture]]\n</code></pre> <p>Process a batch of extracted video frames through the full classification and analysis pipeline.</p> <p>Parameters:</p> Parameter Type Default Description <code>frame_paths</code> <code>List[Union[str, Path]]</code> required Paths to frame images <code>diagrams_dir</code> <code>Optional[Path]</code> <code>None</code> Output directory for diagram files (images, mermaid, JSON) <code>captures_dir</code> <code>Optional[Path]</code> <code>None</code> Output directory for screengrab fallback files <p>Returns: <code>Tuple[List[DiagramResult], List[ScreenCapture]]</code></p> <p>Confidence thresholds:</p> Confidence Range Action >= 0.7 Full diagram analysis -- extracts elements, relationships, Mermaid syntax 0.3 to 0.7 Screengrab fallback -- saves frame with a brief caption < 0.3 Skipped entirely <p>Output files (when directories are provided):</p> <p>For diagrams (<code>diagrams_dir</code>):</p> <ul> <li><code>diagram_N.jpg</code> -- original frame image</li> <li><code>diagram_N.mermaid</code> -- Mermaid source (if generated)</li> <li><code>diagram_N.json</code> -- full DiagramResult as JSON</li> </ul> <p>For screen captures (<code>captures_dir</code>):</p> <ul> <li><code>capture_N.jpg</code> -- original frame image</li> <li><code>capture_N.json</code> -- ScreenCapture metadata as JSON</li> </ul> <pre><code>from pathlib import Path\nfrom video_processor.analyzers.diagram_analyzer import DiagramAnalyzer\nfrom video_processor.providers.manager import ProviderManager\n\nanalyzer = DiagramAnalyzer(\n provider_manager=ProviderManager(),\n confidence_threshold=0.3,\n)\n\nframe_paths = list(Path(\"output/frames\").glob(\"*.jpg\"))\ndiagrams, captures = analyzer.process_frames(\n frame_paths,\n diagrams_dir=Path(\"output/diagrams\"),\n captures_dir=Path(\"output/captures\"),\n)\n\nprint(f\"Found {len(diagrams)} diagrams, {len(captures)} screengrabs\")\nfor d in diagrams:\n print(f\" [{d.diagram_type.value}] {d.description}\")\n</code></pre>"},{"location":"api/analyzers/#contentanalyzer","title":"ContentAnalyzer","text":"<pre><code>from video_processor.analyzers.content_analyzer import ContentAnalyzer\n</code></pre> <p>Cross-references transcript and diagram entities for richer knowledge extraction. Merges entities found in different sources and enriches key points with diagram links.</p>"},{"location":"api/analyzers/#constructor_1","title":"Constructor","text":"<pre><code>def __init__(self, provider_manager: Optional[ProviderManager] = None)\n</code></pre> Parameter Type Default Description <code>provider_manager</code> <code>Optional[ProviderManager]</code> <code>None</code> Required for LLM-based fuzzy matching"},{"location":"api/analyzers/#cross_reference","title":"cross_reference()","text":"<pre><code>def cross_reference(\n self,\n transcript_entities: List[Entity],\n diagram_entities: List[Entity],\n) -> List[Entity]\n</code></pre> <p>Merge entities from transcripts and diagrams into a unified list with source attribution.</p> <p>Merge strategy:</p> <ol> <li>Index all transcript entities by lowercase name, marked with <code>source=\"transcript\"</code></li> <li>Merge diagram entities: if a name matches, set <code>source=\"both\"</code> and combine descriptions/occurrences; otherwise add as <code>source=\"diagram\"</code></li> <li>If a <code>ProviderManager</code> is available, use LLM fuzzy matching to find additional matches among unmatched entities (e.g., \"PostgreSQL\" from transcript matching \"Postgres\" from diagram)</li> </ol> <p>Parameters:</p> Parameter Type Description <code>transcript_entities</code> <code>List[Entity]</code> Entities extracted from transcript <code>diagram_entities</code> <code>List[Entity]</code> Entities extracted from diagrams <p>Returns: <code>List[Entity]</code> -- merged entity list with <code>source</code> attribution.</p> <pre><code>from video_processor.analyzers.content_analyzer import ContentAnalyzer\nfrom video_processor.models import Entity\n\nanalyzer = ContentAnalyzer(provider_manager=pm)\n\ntranscript_entities = [\n Entity(name=\"PostgreSQL\", type=\"technology\"),\n Entity(name=\"Alice\", type=\"person\"),\n]\ndiagram_entities = [\n Entity(name=\"Postgres\", type=\"technology\"),\n Entity(name=\"Redis\", type=\"technology\"),\n]\n\nmerged = analyzer.cross_reference(transcript_entities, diagram_entities)\n# \"PostgreSQL\" and \"Postgres\" may be fuzzy-matched and merged\n</code></pre>"},{"location":"api/analyzers/#enrich_key_points","title":"enrich_key_points()","text":"<pre><code>def enrich_key_points(\n self,\n key_points: List[KeyPoint],\n diagrams: list,\n transcript_text: str,\n) -> List[KeyPoint]\n</code></pre> <p>Link key points to relevant diagrams by entity overlap. Examines word overlap between key point text and diagram elements/text content.</p> <p>Parameters:</p> Parameter Type Description <code>key_points</code> <code>List[KeyPoint]</code> Key points to enrich <code>diagrams</code> <code>list</code> List of <code>DiagramResult</code> objects or dicts <code>transcript_text</code> <code>str</code> Full transcript text (reserved for future use) <p>Returns: <code>List[KeyPoint]</code> -- key points with <code>related_diagrams</code> indices populated.</p> <p>A key point is linked to a diagram when they share 2 or more words (excluding short words) between the key point text/details and the diagram's elements/text content.</p>"},{"location":"api/analyzers/#actiondetector","title":"ActionDetector","text":"<pre><code>from video_processor.analyzers.action_detector import ActionDetector\n</code></pre> <p>Detects action items from transcripts and diagram content using LLM extraction with a regex pattern fallback.</p>"},{"location":"api/analyzers/#constructor_2","title":"Constructor","text":"<pre><code>def __init__(self, provider_manager: Optional[ProviderManager] = None)\n</code></pre> Parameter Type Default Description <code>provider_manager</code> <code>Optional[ProviderManager]</code> <code>None</code> Required for LLM-based extraction"},{"location":"api/analyzers/#detect_from_transcript","title":"detect_from_transcript()","text":"<pre><code>def detect_from_transcript(\n self,\n text: str,\n segments: Optional[List[TranscriptSegment]] = None,\n) -> List[ActionItem]\n</code></pre> <p>Detect action items from transcript text.</p> <p>Parameters:</p> Parameter Type Default Description <code>text</code> <code>str</code> required Transcript text to analyze <code>segments</code> <code>Optional[List[TranscriptSegment]]</code> <code>None</code> Transcript segments for timestamp attachment <p>Returns: <code>List[ActionItem]</code> -- detected action items with <code>source=\"transcript\"</code>.</p> <p>Extraction modes:</p> <ul> <li>LLM mode (when <code>provider_manager</code> is set): Sends the transcript to the LLM with a structured extraction prompt. Extracts action, assignee, deadline, priority, and context.</li> <li>Pattern mode (fallback): Matches sentences against regex patterns for action-oriented language.</li> </ul> <p>Pattern matching detects sentences containing:</p> <ul> <li>\"need/needs to\", \"should/must/shall\"</li> <li>\"will/going to\", \"action item/todo/follow-up\"</li> <li>\"assigned to/responsible for\", \"deadline/due by\"</li> <li>\"let's/let us\", \"make sure/ensure\"</li> <li>\"can you/could you/please\"</li> </ul> <p>Timestamp attachment: When <code>segments</code> are provided, each action item is matched to the most relevant transcript segment (by word overlap, minimum 3 matching words), and a timestamp is added to <code>context</code>.</p>"},{"location":"api/analyzers/#detect_from_diagrams","title":"detect_from_diagrams()","text":"<pre><code>def detect_from_diagrams(self, diagrams: list) -> List[ActionItem]\n</code></pre> <p>Extract action items from diagram text content and elements. Processes each diagram's combined text using either LLM or pattern extraction.</p> <p>Parameters:</p> Parameter Type Description <code>diagrams</code> <code>list</code> List of <code>DiagramResult</code> objects or dicts <p>Returns: <code>List[ActionItem]</code> -- action items with <code>source=\"diagram\"</code>.</p>"},{"location":"api/analyzers/#merge_action_items","title":"merge_action_items()","text":"<pre><code>def merge_action_items(\n self,\n transcript_items: List[ActionItem],\n diagram_items: List[ActionItem],\n) -> List[ActionItem]\n</code></pre> <p>Merge action items from multiple sources, deduplicating by action text (case-insensitive, whitespace-normalized).</p> <p>Returns: <code>List[ActionItem]</code> -- deduplicated merged list.</p>"},{"location":"api/analyzers/#usage-example","title":"Usage example","text":"<pre><code>from video_processor.analyzers.action_detector import ActionDetector\nfrom video_processor.providers.manager import ProviderManager\n\ndetector = ActionDetector(provider_manager=ProviderManager())\n\n# From transcript\ntranscript_items = detector.detect_from_transcript(\n text=\"Alice needs to update the API docs by Friday. \"\n \"Bob should review the PR before merging.\",\n segments=transcript_segments,\n)\n\n# From diagrams\ndiagram_items = detector.detect_from_diagrams(diagram_results)\n\n# Merge and deduplicate\nall_items = detector.merge_action_items(transcript_items, diagram_items)\n\nfor item in all_items:\n print(f\"[{item.priority or 'unset'}] {item.action}\")\n if item.assignee:\n print(f\" Assignee: {item.assignee}\")\n if item.deadline:\n print(f\" Deadline: {item.deadline}\")\n</code></pre>"},{"location":"api/analyzers/#pattern-fallback-no-llm","title":"Pattern fallback (no LLM)","text":"<pre><code># Works without any API keys\ndetector = ActionDetector() # No provider_manager\nitems = detector.detect_from_transcript(\n \"We need to finalize the database schema. \"\n \"Please update the deployment scripts.\"\n)\n# Returns ActionItems matched by regex patterns\n</code></pre>"},{"location":"api/auth/","title":"Auth API Reference","text":""},{"location":"api/auth/#video_processor.auth","title":"<code>video_processor.auth</code>","text":"<p>Unified OAuth and authentication strategy for PlanOpticon connectors.</p> <p>Provides a consistent auth pattern across all source connectors: 1. Saved token (auto-refresh if expired) 2. OAuth 2.0 (Authorization Code with PKCE, or Client Credentials) 3. API key fallback (environment variable)</p> <p>Usage in a connector:</p> <pre><code>from video_processor.auth import OAuthManager, AuthConfig\n\nconfig = AuthConfig(\n service=\"notion\",\n oauth_authorize_url=\"https://api.notion.com/v1/oauth/authorize\",\n oauth_token_url=\"https://api.notion.com/v1/oauth/token\",\n client_id_env=\"NOTION_CLIENT_ID\",\n client_secret_env=\"NOTION_CLIENT_SECRET\",\n api_key_env=\"NOTION_API_KEY\",\n scopes=[\"read_content\"],\n)\nmanager = OAuthManager(config)\ntoken = manager.authenticate() # Returns access token or None\n</code></pre>"},{"location":"api/auth/#video_processor.auth.AuthConfig","title":"<code>AuthConfig</code> <code>dataclass</code>","text":"<p>Configuration for a service's authentication.</p> Source code in <code>video_processor/auth.py</code> <pre><code>@dataclass\nclass AuthConfig:\n \"\"\"Configuration for a service's authentication.\"\"\"\n\n service: str\n\n # OAuth endpoints (set both for OAuth support)\n oauth_authorize_url: Optional[str] = None\n oauth_token_url: Optional[str] = None\n\n # Client credentials (checked from env if not provided)\n client_id: Optional[str] = None\n client_secret: Optional[str] = None\n client_id_env: Optional[str] = None\n client_secret_env: Optional[str] = None\n\n # API key fallback\n api_key_env: Optional[str] = None\n\n # OAuth scopes\n scopes: List[str] = field(default_factory=list)\n\n # Redirect URI for auth code flow\n redirect_uri: str = \"urn:ietf:wg:oauth:2.0:oob\"\n\n # Server-to-Server (client credentials grant)\n account_id: Optional[str] = None\n account_id_env: Optional[str] = None\n\n # Token storage\n token_path: Optional[Path] = None\n\n @property\n def resolved_client_id(self) -> Optional[str]:\n return (\n self.client_id\n or (os.environ.get(self.client_id_env, \"\") if self.client_id_env else None)\n or None\n )\n\n @property\n def resolved_client_secret(self) -> Optional[str]:\n return (\n self.client_secret\n or (os.environ.get(self.client_secret_env, \"\") if self.client_secret_env else None)\n or None\n )\n\n @property\n def resolved_api_key(self) -> Optional[str]:\n if self.api_key_env:\n val = os.environ.get(self.api_key_env, \"\")\n return val if val else None\n return None\n\n @property\n def resolved_account_id(self) -> Optional[str]:\n return (\n self.account_id\n or (os.environ.get(self.account_id_env, \"\") if self.account_id_env else None)\n or None\n )\n\n @property\n def resolved_token_path(self) -> Path:\n return self.token_path or TOKEN_DIR / f\"{self.service}_token.json\"\n\n @property\n def supports_oauth(self) -> bool:\n return bool(self.oauth_authorize_url and self.oauth_token_url)\n</code></pre>"},{"location":"api/auth/#video_processor.auth.AuthResult","title":"<code>AuthResult</code> <code>dataclass</code>","text":"<p>Result of an authentication attempt.</p> Source code in <code>video_processor/auth.py</code> <pre><code>@dataclass\nclass AuthResult:\n \"\"\"Result of an authentication attempt.\"\"\"\n\n success: bool\n access_token: Optional[str] = None\n method: Optional[str] = None # \"saved_token\", \"oauth_pkce\", \"client_credentials\", \"api_key\"\n expires_at: Optional[float] = None\n refresh_token: Optional[str] = None\n error: Optional[str] = None\n</code></pre>"},{"location":"api/auth/#video_processor.auth.OAuthManager","title":"<code>OAuthManager</code>","text":"<p>Manages OAuth and API key authentication for a service.</p> <p>Tries auth methods in order: 1. Load saved token (refresh if expired) 2. Client Credentials grant (if account_id is set) 3. OAuth2 Authorization Code with PKCE (interactive) 4. API key fallback</p> Source code in <code>video_processor/auth.py</code> <pre><code>class OAuthManager:\n \"\"\"Manages OAuth and API key authentication for a service.\n\n Tries auth methods in order:\n 1. Load saved token (refresh if expired)\n 2. Client Credentials grant (if account_id is set)\n 3. OAuth2 Authorization Code with PKCE (interactive)\n 4. API key fallback\n \"\"\"\n\n def __init__(self, config: AuthConfig):\n self.config = config\n self._token_data: Optional[Dict] = None\n\n def authenticate(self) -> AuthResult:\n \"\"\"Run the auth chain and return the result.\"\"\"\n # 1. Saved token\n result = self._try_saved_token()\n if result.success:\n return result\n\n # 2. Client Credentials (Server-to-Server)\n if self.config.resolved_account_id and self.config.supports_oauth:\n result = self._try_client_credentials()\n if result.success:\n return result\n\n # 3. OAuth PKCE (interactive)\n if self.config.supports_oauth and self.config.resolved_client_id:\n result = self._try_oauth_pkce()\n if result.success:\n return result\n\n # 4. API key fallback\n api_key = self.config.resolved_api_key\n if api_key:\n return AuthResult(\n success=True,\n access_token=api_key,\n method=\"api_key\",\n )\n\n # Build a helpful error message\n hints = []\n if self.config.supports_oauth and self.config.client_id_env:\n hints.append(f\"Set {self.config.client_id_env} for OAuth\")\n if self.config.client_secret_env:\n hints.append(f\"and {self.config.client_secret_env}\")\n if self.config.api_key_env:\n hints.append(f\"or set {self.config.api_key_env} for API key access\")\n hint_str = (\" (\" + \" \".join(hints) + \")\") if hints else \"\"\n\n return AuthResult(\n success=False,\n error=f\"No auth method available for {self.config.service}.{hint_str}\",\n )\n\n def get_token(self) -> Optional[str]:\n \"\"\"Convenience: authenticate and return just the token.\"\"\"\n result = self.authenticate()\n return result.access_token if result.success else None\n\n def _try_saved_token(self) -> AuthResult:\n \"\"\"Load and validate a saved token.\"\"\"\n token_path = self.config.resolved_token_path\n if not token_path.exists():\n return AuthResult(success=False)\n\n try:\n data = json.loads(token_path.read_text())\n expires_at = data.get(\"expires_at\", 0)\n\n if time.time() < expires_at:\n self._token_data = data\n return AuthResult(\n success=True,\n access_token=data[\"access_token\"],\n method=\"saved_token\",\n expires_at=expires_at,\n )\n\n # Expired \u2014 try refresh\n if data.get(\"refresh_token\"):\n return self._refresh_token(data)\n\n return AuthResult(success=False)\n except Exception as exc:\n logger.debug(\"Failed to load saved token for %s: %s\", self.config.service, exc)\n return AuthResult(success=False)\n\n def _refresh_token(self, data: Dict) -> AuthResult:\n \"\"\"Refresh an expired OAuth token.\"\"\"\n try:\n import requests\n except ImportError:\n return AuthResult(success=False, error=\"requests not installed\")\n\n client_id = data.get(\"client_id\") or self.config.resolved_client_id\n client_secret = data.get(\"client_secret\") or self.config.resolved_client_secret\n\n if not client_id or not data.get(\"refresh_token\"):\n return AuthResult(success=False)\n\n try:\n resp = requests.post(\n self.config.oauth_token_url,\n data={\n \"grant_type\": \"refresh_token\",\n \"refresh_token\": data[\"refresh_token\"],\n },\n auth=(client_id, client_secret or \"\"),\n timeout=30,\n )\n resp.raise_for_status()\n token_data = resp.json()\n\n new_data = {\n \"access_token\": token_data[\"access_token\"],\n \"refresh_token\": token_data.get(\"refresh_token\", data[\"refresh_token\"]),\n \"expires_at\": time.time() + token_data.get(\"expires_in\", 3600) - 60,\n \"client_id\": client_id,\n \"client_secret\": client_secret or \"\",\n }\n self._save_token(new_data)\n self._token_data = new_data\n\n logger.info(\"Refreshed OAuth token for %s\", self.config.service)\n return AuthResult(\n success=True,\n access_token=new_data[\"access_token\"],\n method=\"saved_token\",\n expires_at=new_data[\"expires_at\"],\n refresh_token=new_data[\"refresh_token\"],\n )\n except Exception as exc:\n logger.debug(\"Token refresh failed for %s: %s\", self.config.service, exc)\n return AuthResult(success=False)\n\n def _try_client_credentials(self) -> AuthResult:\n \"\"\"Server-to-Server OAuth using client credentials grant.\"\"\"\n try:\n import requests\n except ImportError:\n return AuthResult(success=False, error=\"requests not installed\")\n\n client_id = self.config.resolved_client_id\n client_secret = self.config.resolved_client_secret\n account_id = self.config.resolved_account_id\n\n if not client_id or not client_secret:\n return AuthResult(success=False)\n\n try:\n resp = requests.post(\n self.config.oauth_token_url,\n params={\n \"grant_type\": \"account_credentials\",\n \"account_id\": account_id,\n },\n auth=(client_id, client_secret),\n timeout=30,\n )\n resp.raise_for_status()\n token_data = resp.json()\n\n data = {\n \"access_token\": token_data[\"access_token\"],\n \"expires_at\": time.time() + token_data.get(\"expires_in\", 3600) - 60,\n }\n self._save_token(data)\n self._token_data = data\n\n logger.info(\"Authenticated %s via client credentials\", self.config.service)\n return AuthResult(\n success=True,\n access_token=data[\"access_token\"],\n method=\"client_credentials\",\n expires_at=data[\"expires_at\"],\n )\n except Exception as exc:\n logger.debug(\"Client credentials failed for %s: %s\", self.config.service, exc)\n return AuthResult(success=False)\n\n def _try_oauth_pkce(self) -> AuthResult:\n \"\"\"Interactive OAuth2 Authorization Code flow with PKCE.\"\"\"\n try:\n import requests\n except ImportError:\n return AuthResult(success=False, error=\"requests not installed\")\n\n client_id = self.config.resolved_client_id\n if not client_id:\n return AuthResult(success=False)\n\n # Generate PKCE verifier and challenge\n code_verifier = secrets.token_urlsafe(64)\n code_challenge = (\n base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode(\"ascii\")).digest())\n .rstrip(b\"=\")\n .decode(\"ascii\")\n )\n\n # Build authorize URL\n params = (\n f\"?response_type=code\"\n f\"&client_id={client_id}\"\n f\"&redirect_uri={self.config.redirect_uri}\"\n f\"&code_challenge={code_challenge}\"\n f\"&code_challenge_method=S256\"\n )\n if self.config.scopes:\n params += f\"&scope={'+'.join(self.config.scopes)}\"\n\n authorize_url = f\"{self.config.oauth_authorize_url}{params}\"\n\n print(f\"\\nOpen this URL to authorize PlanOpticon ({self.config.service}):\")\n print(f\"{authorize_url}\\n\")\n\n try:\n webbrowser.open(authorize_url)\n except Exception:\n pass\n\n try:\n auth_code = input(\"Enter the authorization code: \").strip()\n except (KeyboardInterrupt, EOFError):\n return AuthResult(success=False, error=\"Auth cancelled by user\")\n\n if not auth_code:\n return AuthResult(success=False, error=\"No auth code provided\")\n\n # Exchange code for tokens\n client_secret = self.config.resolved_client_secret\n try:\n resp = requests.post(\n self.config.oauth_token_url,\n data={\n \"grant_type\": \"authorization_code\",\n \"code\": auth_code,\n \"redirect_uri\": self.config.redirect_uri,\n \"code_verifier\": code_verifier,\n },\n auth=(client_id, client_secret or \"\"),\n timeout=30,\n )\n resp.raise_for_status()\n token_data = resp.json()\n\n data = {\n \"access_token\": token_data[\"access_token\"],\n \"refresh_token\": token_data.get(\"refresh_token\"),\n \"expires_at\": time.time() + token_data.get(\"expires_in\", 3600) - 60,\n \"client_id\": client_id,\n \"client_secret\": client_secret or \"\",\n }\n self._save_token(data)\n self._token_data = data\n\n logger.info(\"Authenticated %s via OAuth PKCE\", self.config.service)\n return AuthResult(\n success=True,\n access_token=data[\"access_token\"],\n method=\"oauth_pkce\",\n expires_at=data[\"expires_at\"],\n refresh_token=data.get(\"refresh_token\"),\n )\n except Exception as exc:\n logger.debug(\"OAuth PKCE failed for %s: %s\", self.config.service, exc)\n return AuthResult(success=False, error=str(exc))\n\n def _save_token(self, data: Dict) -> None:\n \"\"\"Persist token data to disk.\"\"\"\n token_path = self.config.resolved_token_path\n token_path.parent.mkdir(parents=True, exist_ok=True)\n token_path.write_text(json.dumps(data))\n logger.info(\"Saved %s token to %s\", self.config.service, token_path)\n\n def clear_token(self) -> None:\n \"\"\"Remove saved token (logout).\"\"\"\n token_path = self.config.resolved_token_path\n if token_path.exists():\n token_path.unlink()\n logger.info(\"Cleared %s token\", self.config.service)\n</code></pre>"},{"location":"api/auth/#video_processor.auth.OAuthManager.authenticate","title":"<code>authenticate()</code>","text":"<p>Run the auth chain and return the result.</p> Source code in <code>video_processor/auth.py</code> <pre><code>def authenticate(self) -> AuthResult:\n \"\"\"Run the auth chain and return the result.\"\"\"\n # 1. Saved token\n result = self._try_saved_token()\n if result.success:\n return result\n\n # 2. Client Credentials (Server-to-Server)\n if self.config.resolved_account_id and self.config.supports_oauth:\n result = self._try_client_credentials()\n if result.success:\n return result\n\n # 3. OAuth PKCE (interactive)\n if self.config.supports_oauth and self.config.resolved_client_id:\n result = self._try_oauth_pkce()\n if result.success:\n return result\n\n # 4. API key fallback\n api_key = self.config.resolved_api_key\n if api_key:\n return AuthResult(\n success=True,\n access_token=api_key,\n method=\"api_key\",\n )\n\n # Build a helpful error message\n hints = []\n if self.config.supports_oauth and self.config.client_id_env:\n hints.append(f\"Set {self.config.client_id_env} for OAuth\")\n if self.config.client_secret_env:\n hints.append(f\"and {self.config.client_secret_env}\")\n if self.config.api_key_env:\n hints.append(f\"or set {self.config.api_key_env} for API key access\")\n hint_str = (\" (\" + \" \".join(hints) + \")\") if hints else \"\"\n\n return AuthResult(\n success=False,\n error=f\"No auth method available for {self.config.service}.{hint_str}\",\n )\n</code></pre>"},{"location":"api/auth/#video_processor.auth.OAuthManager.clear_token","title":"<code>clear_token()</code>","text":"<p>Remove saved token (logout).</p> Source code in <code>video_processor/auth.py</code> <pre><code>def clear_token(self) -> None:\n \"\"\"Remove saved token (logout).\"\"\"\n token_path = self.config.resolved_token_path\n if token_path.exists():\n token_path.unlink()\n logger.info(\"Cleared %s token\", self.config.service)\n</code></pre>"},{"location":"api/auth/#video_processor.auth.OAuthManager.get_token","title":"<code>get_token()</code>","text":"<p>Convenience: authenticate and return just the token.</p> Source code in <code>video_processor/auth.py</code> <pre><code>def get_token(self) -> Optional[str]:\n \"\"\"Convenience: authenticate and return just the token.\"\"\"\n result = self.authenticate()\n return result.access_token if result.success else None\n</code></pre>"},{"location":"api/auth/#video_processor.auth.get_auth_config","title":"<code>get_auth_config(service)</code>","text":"<p>Get a pre-built AuthConfig for a known service.</p> Source code in <code>video_processor/auth.py</code> <pre><code>def get_auth_config(service: str) -> Optional[AuthConfig]:\n \"\"\"Get a pre-built AuthConfig for a known service.\"\"\"\n return KNOWN_CONFIGS.get(service)\n</code></pre>"},{"location":"api/auth/#video_processor.auth.get_auth_manager","title":"<code>get_auth_manager(service)</code>","text":"<p>Get an OAuthManager for a known service.</p> Source code in <code>video_processor/auth.py</code> <pre><code>def get_auth_manager(service: str) -> Optional[OAuthManager]:\n \"\"\"Get an OAuthManager for a known service.\"\"\"\n config = get_auth_config(service)\n if config:\n return OAuthManager(config)\n return None\n</code></pre>"},{"location":"api/auth/#overview","title":"Overview","text":"<p>The <code>video_processor.auth</code> module provides a unified OAuth and authentication strategy for all PlanOpticon source connectors. It supports multiple authentication methods tried in a consistent order:</p> <ol> <li>Saved token -- load from disk, auto-refresh if expired</li> <li>Client Credentials -- server-to-server OAuth (e.g., Zoom S2S)</li> <li>OAuth 2.0 PKCE -- interactive Authorization Code flow with PKCE</li> <li>API key fallback -- environment variable lookup</li> </ol> <p>Tokens are persisted to <code>~/.planopticon/</code> and automatically refreshed on expiry.</p>"},{"location":"api/auth/#authconfig","title":"AuthConfig","text":"<pre><code>from video_processor.auth import AuthConfig\n</code></pre> <p>Dataclass configuring authentication for a specific service. Defines OAuth endpoints, client credentials, API key fallback, scopes, and token storage.</p>"},{"location":"api/auth/#fields","title":"Fields","text":"Field Type Default Description <code>service</code> <code>str</code> required Service identifier (e.g., <code>\"zoom\"</code>, <code>\"notion\"</code>) <code>oauth_authorize_url</code> <code>Optional[str]</code> <code>None</code> OAuth authorization endpoint URL <code>oauth_token_url</code> <code>Optional[str]</code> <code>None</code> OAuth token exchange endpoint URL <code>client_id</code> <code>Optional[str]</code> <code>None</code> OAuth client ID (direct value) <code>client_secret</code> <code>Optional[str]</code> <code>None</code> OAuth client secret (direct value) <code>client_id_env</code> <code>Optional[str]</code> <code>None</code> Environment variable for client ID <code>client_secret_env</code> <code>Optional[str]</code> <code>None</code> Environment variable for client secret <code>api_key_env</code> <code>Optional[str]</code> <code>None</code> Environment variable for API key fallback <code>scopes</code> <code>List[str]</code> <code>[]</code> OAuth scopes to request <code>redirect_uri</code> <code>str</code> <code>\"urn:ietf:wg:oauth:2.0:oob\"</code> Redirect URI for auth code flow <code>account_id</code> <code>Optional[str]</code> <code>None</code> Account ID for client credentials grant (direct value) <code>account_id_env</code> <code>Optional[str]</code> <code>None</code> Environment variable for account ID <code>token_path</code> <code>Optional[Path]</code> <code>None</code> Custom token storage path"},{"location":"api/auth/#resolved-properties","title":"Resolved Properties","text":"<p>These properties resolve values by checking the direct field first, then falling back to the environment variable.</p> Property Return Type Description <code>resolved_client_id</code> <code>Optional[str]</code> Client ID from <code>client_id</code> or <code>os.environ[client_id_env]</code> <code>resolved_client_secret</code> <code>Optional[str]</code> Client secret from <code>client_secret</code> or <code>os.environ[client_secret_env]</code> <code>resolved_api_key</code> <code>Optional[str]</code> API key from <code>os.environ[api_key_env]</code> <code>resolved_account_id</code> <code>Optional[str]</code> Account ID from <code>account_id</code> or <code>os.environ[account_id_env]</code> <code>resolved_token_path</code> <code>Path</code> Token file path: <code>token_path</code> or <code>~/.planopticon/{service}_token.json</code> <code>supports_oauth</code> <code>bool</code> <code>True</code> if both <code>oauth_authorize_url</code> and <code>oauth_token_url</code> are set <pre><code>from video_processor.auth import AuthConfig\n\nconfig = AuthConfig(\n service=\"notion\",\n oauth_authorize_url=\"https://api.notion.com/v1/oauth/authorize\",\n oauth_token_url=\"https://api.notion.com/v1/oauth/token\",\n client_id_env=\"NOTION_CLIENT_ID\",\n client_secret_env=\"NOTION_CLIENT_SECRET\",\n api_key_env=\"NOTION_API_KEY\",\n scopes=[\"read_content\"],\n)\n\n# Check resolved values\nprint(config.resolved_client_id) # From NOTION_CLIENT_ID env var\nprint(config.supports_oauth) # True\nprint(config.resolved_token_path) # ~/.planopticon/notion_token.json\n</code></pre>"},{"location":"api/auth/#authresult","title":"AuthResult","text":"<pre><code>from video_processor.auth import AuthResult\n</code></pre> <p>Dataclass representing the result of an authentication attempt.</p> Field Type Default Description <code>success</code> <code>bool</code> required Whether authentication succeeded <code>access_token</code> <code>Optional[str]</code> <code>None</code> The access token (if successful) <code>method</code> <code>Optional[str]</code> <code>None</code> Auth method used: <code>\"saved_token\"</code>, <code>\"oauth_pkce\"</code>, <code>\"client_credentials\"</code>, <code>\"api_key\"</code> <code>expires_at</code> <code>Optional[float]</code> <code>None</code> Token expiration as Unix timestamp <code>refresh_token</code> <code>Optional[str]</code> <code>None</code> OAuth refresh token (if available) <code>error</code> <code>Optional[str]</code> <code>None</code> Error message (if failed) <pre><code>result = manager.authenticate()\nif result.success:\n print(f\"Authenticated via {result.method}\")\n print(f\"Token: {result.access_token[:20]}...\")\n if result.expires_at:\n import time\n remaining = result.expires_at - time.time()\n print(f\"Expires in {remaining/60:.0f} minutes\")\nelse:\n print(f\"Auth failed: {result.error}\")\n</code></pre>"},{"location":"api/auth/#oauthmanager","title":"OAuthManager","text":"<pre><code>from video_processor.auth import OAuthManager\n</code></pre> <p>Manages the full authentication lifecycle for a service. Tries auth methods in priority order and handles token persistence, refresh, and PKCE flow.</p>"},{"location":"api/auth/#constructor","title":"Constructor","text":"<pre><code>def __init__(self, config: AuthConfig)\n</code></pre> Parameter Type Description <code>config</code> <code>AuthConfig</code> Authentication configuration for the target service"},{"location":"api/auth/#authenticate","title":"authenticate()","text":"<pre><code>def authenticate(self) -> AuthResult\n</code></pre> <p>Run the full auth chain and return the result. Methods are tried in order:</p> <ol> <li>Saved token -- checks <code>~/.planopticon/{service}_token.json</code>, refreshes if expired</li> <li>Client Credentials -- if <code>account_id</code> is set and OAuth is configured, uses the client credentials grant (server-to-server)</li> <li>OAuth PKCE -- if OAuth is configured and client ID is available, opens a browser for interactive authorization with PKCE</li> <li>API key -- falls back to the environment variable specified in <code>api_key_env</code></li> </ol> <p>Returns: <code>AuthResult</code> -- success/failure with token and method details.</p> <p>If all methods fail, returns an <code>AuthResult</code> with <code>success=False</code> and a helpful error message listing which environment variables to set.</p>"},{"location":"api/auth/#get_token","title":"get_token()","text":"<pre><code>def get_token(self) -> Optional[str]\n</code></pre> <p>Convenience method: run <code>authenticate()</code> and return just the access token string.</p> <p>Returns: <code>Optional[str]</code> -- the access token, or <code>None</code> if authentication failed.</p>"},{"location":"api/auth/#clear_token","title":"clear_token()","text":"<pre><code>def clear_token(self) -> None\n</code></pre> <p>Remove the saved token file for this service (effectively a logout). The next <code>authenticate()</code> call will require re-authentication.</p>"},{"location":"api/auth/#authentication-flows","title":"Authentication Flows","text":""},{"location":"api/auth/#saved-token-auto-refresh","title":"Saved Token (auto-refresh)","text":"<p>Tokens are saved to <code>~/.planopticon/{service}_token.json</code> as JSON. On each <code>authenticate()</code> call, the saved token is loaded and checked:</p> <ul> <li>If the token has not expired (<code>time.time() < expires_at</code>), it is returned immediately</li> <li>If expired but a refresh token is available, the manager attempts to refresh using the OAuth token endpoint</li> <li>The refreshed token is saved back to disk</li> </ul>"},{"location":"api/auth/#client-credentials-grant","title":"Client Credentials Grant","text":"<p>Used for server-to-server authentication (e.g., Zoom Server-to-Server OAuth). Requires <code>account_id</code>, <code>client_id</code>, and <code>client_secret</code>. Sends a POST to the token endpoint with <code>grant_type=account_credentials</code>.</p>"},{"location":"api/auth/#oauth-20-authorization-code-with-pkce","title":"OAuth 2.0 Authorization Code with PKCE","text":"<p>Interactive flow for user authentication:</p> <ol> <li>Generates a PKCE code verifier and S256 challenge</li> <li>Constructs the authorization URL with client ID, redirect URI, scopes, and PKCE challenge</li> <li>Opens the URL in the user's browser</li> <li>Prompts the user to paste the authorization code</li> <li>Exchanges the code for tokens at the token endpoint</li> <li>Saves the tokens to disk</li> </ol>"},{"location":"api/auth/#api-key-fallback","title":"API Key Fallback","text":"<p>If no OAuth flow succeeds, falls back to checking the environment variable specified in <code>api_key_env</code>. Returns the value directly as the access token.</p>"},{"location":"api/auth/#known_configs","title":"KNOWN_CONFIGS","text":"<pre><code>from video_processor.auth import KNOWN_CONFIGS\n</code></pre> <p>Pre-built <code>AuthConfig</code> instances for supported services. These cover the most common cloud integrations and can be used directly or as templates for custom configurations.</p> Service Key Service OAuth Endpoints Client ID Env API Key Env <code>\"zoom\"</code> Zoom <code>zoom.us/oauth/...</code> <code>ZOOM_CLIENT_ID</code> -- <code>\"notion\"</code> Notion <code>api.notion.com/v1/oauth/...</code> <code>NOTION_CLIENT_ID</code> <code>NOTION_API_KEY</code> <code>\"dropbox\"</code> Dropbox <code>dropbox.com/oauth2/...</code> <code>DROPBOX_APP_KEY</code> <code>DROPBOX_ACCESS_TOKEN</code> <code>\"github\"</code> GitHub <code>github.com/login/oauth/...</code> <code>GITHUB_CLIENT_ID</code> <code>GITHUB_TOKEN</code> <code>\"google\"</code> Google <code>accounts.google.com/o/oauth2/...</code> <code>GOOGLE_CLIENT_ID</code> <code>GOOGLE_API_KEY</code> <code>\"microsoft\"</code> Microsoft <code>login.microsoftonline.com/.../oauth2/...</code> <code>MICROSOFT_CLIENT_ID</code> --"},{"location":"api/auth/#zoom","title":"Zoom","text":"<p>Supports both Server-to-Server (via <code>ZOOM_ACCOUNT_ID</code>) and OAuth PKCE flows.</p> <pre><code># Server-to-Server\nexport ZOOM_CLIENT_ID=\"...\"\nexport ZOOM_CLIENT_SECRET=\"...\"\nexport ZOOM_ACCOUNT_ID=\"...\"\n\n# Or interactive OAuth (omit ZOOM_ACCOUNT_ID)\nexport ZOOM_CLIENT_ID=\"...\"\nexport ZOOM_CLIENT_SECRET=\"...\"\n</code></pre>"},{"location":"api/auth/#google-drive-meet-workspace","title":"Google (Drive, Meet, Workspace)","text":"<p>Supports OAuth PKCE and API key fallback. Scopes include Drive and Docs read-only access.</p> <pre><code>export GOOGLE_CLIENT_ID=\"...\"\nexport GOOGLE_CLIENT_SECRET=\"...\"\n# Or for API-key-only access:\nexport GOOGLE_API_KEY=\"...\"\n</code></pre>"},{"location":"api/auth/#github","title":"GitHub","text":"<p>Supports OAuth PKCE and personal access token. Requests <code>repo</code> and <code>read:org</code> scopes.</p> <pre><code># OAuth\nexport GITHUB_CLIENT_ID=\"...\"\nexport GITHUB_CLIENT_SECRET=\"...\"\n# Or personal access token\nexport GITHUB_TOKEN=\"ghp_...\"\n</code></pre>"},{"location":"api/auth/#helper-functions","title":"Helper Functions","text":""},{"location":"api/auth/#get_auth_config","title":"get_auth_config()","text":"<pre><code>def get_auth_config(service: str) -> Optional[AuthConfig]\n</code></pre> <p>Get a pre-built <code>AuthConfig</code> for a known service.</p> <p>Parameters:</p> Parameter Type Description <code>service</code> <code>str</code> Service name (e.g., <code>\"zoom\"</code>, <code>\"notion\"</code>, <code>\"github\"</code>) <p>Returns: <code>Optional[AuthConfig]</code> -- the config, or <code>None</code> if the service is not in <code>KNOWN_CONFIGS</code>.</p>"},{"location":"api/auth/#get_auth_manager","title":"get_auth_manager()","text":"<pre><code>def get_auth_manager(service: str) -> Optional[OAuthManager]\n</code></pre> <p>Get an <code>OAuthManager</code> for a known service. Convenience wrapper that looks up the config and creates the manager in one call.</p> <p>Returns: <code>Optional[OAuthManager]</code> -- the manager, or <code>None</code> if the service is not known.</p>"},{"location":"api/auth/#usage-examples","title":"Usage Examples","text":""},{"location":"api/auth/#quick-authentication-for-a-known-service","title":"Quick authentication for a known service","text":"<pre><code>from video_processor.auth import get_auth_manager\n\nmanager = get_auth_manager(\"zoom\")\nif manager:\n result = manager.authenticate()\n if result.success:\n print(f\"Authenticated via {result.method}\")\n # Use result.access_token for API calls\n else:\n print(f\"Failed: {result.error}\")\n</code></pre>"},{"location":"api/auth/#custom-service-configuration","title":"Custom service configuration","text":"<pre><code>from video_processor.auth import AuthConfig, OAuthManager\n\nconfig = AuthConfig(\n service=\"my_service\",\n oauth_authorize_url=\"https://my-service.com/oauth/authorize\",\n oauth_token_url=\"https://my-service.com/oauth/token\",\n client_id_env=\"MY_SERVICE_CLIENT_ID\",\n client_secret_env=\"MY_SERVICE_CLIENT_SECRET\",\n api_key_env=\"MY_SERVICE_API_KEY\",\n scopes=[\"read\", \"write\"],\n)\n\nmanager = OAuthManager(config)\ntoken = manager.get_token() # Returns str or None\n</code></pre>"},{"location":"api/auth/#using-auth-in-a-custom-source-connector","title":"Using auth in a custom source connector","text":"<pre><code>from pathlib import Path\nfrom typing import List, Optional\n\nfrom video_processor.auth import OAuthManager, AuthConfig\nfrom video_processor.sources.base import BaseSource, SourceFile\n\nclass CustomSource(BaseSource):\n def __init__(self):\n self._config = AuthConfig(\n service=\"custom\",\n api_key_env=\"CUSTOM_API_KEY\",\n )\n self._manager = OAuthManager(self._config)\n self._token: Optional[str] = None\n\n def authenticate(self) -> bool:\n self._token = self._manager.get_token()\n return self._token is not None\n\n def list_videos(self, **kwargs) -> List[SourceFile]:\n # Use self._token to query the API\n ...\n\n def download(self, file: SourceFile, destination: Path) -> Path:\n # Use self._token for authenticated downloads\n ...\n</code></pre>"},{"location":"api/auth/#logout-clear-saved-token","title":"Logout / clear saved token","text":"<pre><code>from video_processor.auth import get_auth_manager\n\nmanager = get_auth_manager(\"zoom\")\nif manager:\n manager.clear_token()\n print(\"Zoom token cleared\")\n</code></pre>"},{"location":"api/auth/#token-storage-location","title":"Token storage location","text":"<p>All tokens are stored under <code>~/.planopticon/</code>:</p> <pre><code>~/.planopticon/\n zoom_token.json\n notion_token.json\n github_token.json\n google_token.json\n microsoft_token.json\n dropbox_token.json\n</code></pre> <p>Each file contains a JSON object with <code>access_token</code>, <code>refresh_token</code> (if applicable), <code>expires_at</code>, and client credentials for refresh.</p>"},{"location":"api/models/","title":"Models API Reference","text":""},{"location":"api/models/#video_processor.models","title":"<code>video_processor.models</code>","text":"<p>Pydantic data models for PlanOpticon output.</p>"},{"location":"api/models/#video_processor.models.ActionItem","title":"<code>ActionItem</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>An action item extracted from content.</p> Source code in <code>video_processor/models.py</code> <pre><code>class ActionItem(BaseModel):\n \"\"\"An action item extracted from content.\"\"\"\n\n action: str = Field(description=\"The action to be taken\")\n assignee: Optional[str] = Field(default=None, description=\"Person responsible\")\n deadline: Optional[str] = Field(default=None, description=\"Deadline or timeframe\")\n priority: Optional[str] = Field(default=None, description=\"Priority level\")\n context: Optional[str] = Field(default=None, description=\"Additional context\")\n source: Optional[str] = Field(\n default=None, description=\"Where this was found (transcript/diagram)\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.BatchManifest","title":"<code>BatchManifest</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Manifest for a batch processing run.</p> Source code in <code>video_processor/models.py</code> <pre><code>class BatchManifest(BaseModel):\n \"\"\"Manifest for a batch processing run.\"\"\"\n\n version: str = Field(default=\"1.0\")\n title: str = Field(default=\"Batch Processing Results\")\n processed_at: str = Field(default_factory=lambda: datetime.now().isoformat())\n stats: ProcessingStats = Field(default_factory=ProcessingStats)\n\n videos: List[BatchVideoEntry] = Field(default_factory=list)\n\n # Aggregated counts\n total_videos: int = Field(default=0)\n completed_videos: int = Field(default=0)\n failed_videos: int = Field(default=0)\n total_diagrams: int = Field(default=0)\n total_action_items: int = Field(default=0)\n total_key_points: int = Field(default=0)\n\n # Batch-level output paths (relative)\n batch_summary_md: Optional[str] = Field(default=None)\n merged_knowledge_graph_json: Optional[str] = Field(default=None)\n merged_knowledge_graph_db: Optional[str] = Field(default=None)\n</code></pre>"},{"location":"api/models/#video_processor.models.BatchVideoEntry","title":"<code>BatchVideoEntry</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Summary of a single video within a batch.</p> Source code in <code>video_processor/models.py</code> <pre><code>class BatchVideoEntry(BaseModel):\n \"\"\"Summary of a single video within a batch.\"\"\"\n\n video_name: str\n manifest_path: str = Field(description=\"Relative path to video manifest\")\n status: str = Field(default=\"pending\", description=\"pending/completed/failed\")\n error: Optional[str] = Field(default=None, description=\"Error message if failed\")\n diagrams_count: int = Field(default=0)\n action_items_count: int = Field(default=0)\n key_points_count: int = Field(default=0)\n duration_seconds: Optional[float] = Field(default=None)\n</code></pre>"},{"location":"api/models/#video_processor.models.DiagramResult","title":"<code>DiagramResult</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Result from diagram extraction and analysis.</p> Source code in <code>video_processor/models.py</code> <pre><code>class DiagramResult(BaseModel):\n \"\"\"Result from diagram extraction and analysis.\"\"\"\n\n frame_index: int = Field(description=\"Index of the source frame\")\n timestamp: Optional[float] = Field(default=None, description=\"Timestamp in video (seconds)\")\n diagram_type: DiagramType = Field(default=DiagramType.unknown, description=\"Type of diagram\")\n confidence: float = Field(default=0.0, description=\"Detection confidence 0-1\")\n description: Optional[str] = Field(default=None, description=\"Description of the diagram\")\n text_content: Optional[str] = Field(default=None, description=\"Text visible in the diagram\")\n elements: List[str] = Field(default_factory=list, description=\"Identified elements\")\n relationships: List[str] = Field(default_factory=list, description=\"Identified relationships\")\n mermaid: Optional[str] = Field(default=None, description=\"Mermaid syntax representation\")\n chart_data: Optional[Dict[str, Any]] = Field(\n default=None, description=\"Chart data for reproduction (labels, values, chart_type)\"\n )\n image_path: Optional[str] = Field(default=None, description=\"Relative path to original frame\")\n svg_path: Optional[str] = Field(default=None, description=\"Relative path to rendered SVG\")\n png_path: Optional[str] = Field(default=None, description=\"Relative path to rendered PNG\")\n mermaid_path: Optional[str] = Field(default=None, description=\"Relative path to mermaid source\")\n</code></pre>"},{"location":"api/models/#video_processor.models.DiagramType","title":"<code>DiagramType</code>","text":"<p> Bases: <code>str</code>, <code>Enum</code></p> <p>Types of visual content detected in video frames.</p> Source code in <code>video_processor/models.py</code> <pre><code>class DiagramType(str, Enum):\n \"\"\"Types of visual content detected in video frames.\"\"\"\n\n flowchart = \"flowchart\"\n sequence = \"sequence\"\n architecture = \"architecture\"\n whiteboard = \"whiteboard\"\n chart = \"chart\"\n table = \"table\"\n slide = \"slide\"\n screenshot = \"screenshot\"\n unknown = \"unknown\"\n</code></pre>"},{"location":"api/models/#video_processor.models.Entity","title":"<code>Entity</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>An entity in the knowledge graph.</p> Source code in <code>video_processor/models.py</code> <pre><code>class Entity(BaseModel):\n \"\"\"An entity in the knowledge graph.\"\"\"\n\n name: str = Field(description=\"Entity name\")\n type: str = Field(default=\"concept\", description=\"Entity type (person, concept, time, diagram)\")\n descriptions: List[str] = Field(default_factory=list, description=\"Descriptions of this entity\")\n source: Optional[str] = Field(\n default=None, description=\"Source attribution (transcript/diagram/both)\"\n )\n occurrences: List[Dict[str, Any]] = Field(\n default_factory=list, description=\"List of occurrences with source, timestamp, text\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.KeyPoint","title":"<code>KeyPoint</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A key point extracted from content.</p> Source code in <code>video_processor/models.py</code> <pre><code>class KeyPoint(BaseModel):\n \"\"\"A key point extracted from content.\"\"\"\n\n point: str = Field(description=\"The key point\")\n topic: Optional[str] = Field(default=None, description=\"Topic or category\")\n details: Optional[str] = Field(default=None, description=\"Supporting details\")\n timestamp: Optional[float] = Field(default=None, description=\"Timestamp in video (seconds)\")\n source: Optional[str] = Field(default=None, description=\"Where this was found\")\n related_diagrams: List[int] = Field(\n default_factory=list, description=\"Indices of related diagrams\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.KnowledgeGraphData","title":"<code>KnowledgeGraphData</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Serializable knowledge graph data.</p> Source code in <code>video_processor/models.py</code> <pre><code>class KnowledgeGraphData(BaseModel):\n \"\"\"Serializable knowledge graph data.\"\"\"\n\n nodes: List[Entity] = Field(default_factory=list, description=\"Graph nodes/entities\")\n relationships: List[Relationship] = Field(\n default_factory=list, description=\"Graph relationships\"\n )\n sources: List[SourceRecord] = Field(\n default_factory=list, description=\"Content sources for provenance tracking\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.OutputFormat","title":"<code>OutputFormat</code>","text":"<p> Bases: <code>str</code>, <code>Enum</code></p> <p>Available output formats.</p> Source code in <code>video_processor/models.py</code> <pre><code>class OutputFormat(str, Enum):\n \"\"\"Available output formats.\"\"\"\n\n markdown = \"markdown\"\n json = \"json\"\n html = \"html\"\n pdf = \"pdf\"\n svg = \"svg\"\n png = \"png\"\n</code></pre>"},{"location":"api/models/#video_processor.models.PlanningEntity","title":"<code>PlanningEntity</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>An entity classified for planning purposes.</p> Source code in <code>video_processor/models.py</code> <pre><code>class PlanningEntity(BaseModel):\n \"\"\"An entity classified for planning purposes.\"\"\"\n\n name: str\n planning_type: PlanningEntityType\n description: str = \"\"\n priority: Optional[str] = None # \"high\", \"medium\", \"low\"\n status: Optional[str] = None # \"identified\", \"confirmed\", \"resolved\"\n source_entities: List[str] = Field(default_factory=list)\n metadata: Dict[str, Any] = Field(default_factory=dict)\n</code></pre>"},{"location":"api/models/#video_processor.models.PlanningEntityType","title":"<code>PlanningEntityType</code>","text":"<p> Bases: <code>str</code>, <code>Enum</code></p> <p>Types of entities in a planning taxonomy.</p> Source code in <code>video_processor/models.py</code> <pre><code>class PlanningEntityType(str, Enum):\n \"\"\"Types of entities in a planning taxonomy.\"\"\"\n\n GOAL = \"goal\"\n REQUIREMENT = \"requirement\"\n CONSTRAINT = \"constraint\"\n DECISION = \"decision\"\n RISK = \"risk\"\n ASSUMPTION = \"assumption\"\n DEPENDENCY = \"dependency\"\n MILESTONE = \"milestone\"\n TASK = \"task\"\n FEATURE = \"feature\"\n</code></pre>"},{"location":"api/models/#video_processor.models.PlanningRelationshipType","title":"<code>PlanningRelationshipType</code>","text":"<p> Bases: <code>str</code>, <code>Enum</code></p> <p>Relationship types within a planning taxonomy.</p> Source code in <code>video_processor/models.py</code> <pre><code>class PlanningRelationshipType(str, Enum):\n \"\"\"Relationship types within a planning taxonomy.\"\"\"\n\n REQUIRES = \"requires\"\n BLOCKED_BY = \"blocked_by\"\n HAS_RISK = \"has_risk\"\n DEPENDS_ON = \"depends_on\"\n ADDRESSES = \"addresses\"\n HAS_TRADEOFF = \"has_tradeoff\"\n DELIVERS = \"delivers\"\n IMPLEMENTS = \"implements\"\n PARENT_OF = \"parent_of\"\n</code></pre>"},{"location":"api/models/#video_processor.models.ProcessingStats","title":"<code>ProcessingStats</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Statistics about a processing run.</p> Source code in <code>video_processor/models.py</code> <pre><code>class ProcessingStats(BaseModel):\n \"\"\"Statistics about a processing run.\"\"\"\n\n start_time: Optional[str] = Field(default=None, description=\"ISO format start time\")\n end_time: Optional[str] = Field(default=None, description=\"ISO format end time\")\n duration_seconds: Optional[float] = Field(default=None, description=\"Total processing time\")\n frames_extracted: int = Field(default=0)\n people_frames_filtered: int = Field(default=0)\n diagrams_detected: int = Field(default=0)\n screen_captures: int = Field(default=0)\n transcript_duration_seconds: Optional[float] = Field(default=None)\n models_used: Dict[str, str] = Field(\n default_factory=dict, description=\"Map of task to model used (e.g. vision: gpt-4o)\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.ProgressCallback","title":"<code>ProgressCallback</code>","text":"<p> Bases: <code>Protocol</code></p> <p>Optional callback for pipeline progress updates.</p> Source code in <code>video_processor/models.py</code> <pre><code>@runtime_checkable\nclass ProgressCallback(Protocol):\n \"\"\"Optional callback for pipeline progress updates.\"\"\"\n\n def on_step_start(self, step: str, index: int, total: int) -> None: ...\n def on_step_complete(self, step: str, index: int, total: int) -> None: ...\n def on_progress(self, step: str, percent: float, message: str = \"\") -> None: ...\n</code></pre>"},{"location":"api/models/#video_processor.models.Relationship","title":"<code>Relationship</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A relationship between entities in the knowledge graph.</p> Source code in <code>video_processor/models.py</code> <pre><code>class Relationship(BaseModel):\n \"\"\"A relationship between entities in the knowledge graph.\"\"\"\n\n source: str = Field(description=\"Source entity name\")\n target: str = Field(description=\"Target entity name\")\n type: str = Field(default=\"related_to\", description=\"Relationship type\")\n content_source: Optional[str] = Field(default=None, description=\"Content source identifier\")\n timestamp: Optional[float] = Field(default=None, description=\"Timestamp in seconds\")\n</code></pre>"},{"location":"api/models/#video_processor.models.ScreenCapture","title":"<code>ScreenCapture</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A screengrab fallback when diagram extraction fails or is uncertain.</p> Source code in <code>video_processor/models.py</code> <pre><code>class ScreenCapture(BaseModel):\n \"\"\"A screengrab fallback when diagram extraction fails or is uncertain.\"\"\"\n\n frame_index: int = Field(description=\"Index of the source frame\")\n timestamp: Optional[float] = Field(default=None, description=\"Timestamp in video (seconds)\")\n caption: Optional[str] = Field(default=None, description=\"Brief description of the content\")\n image_path: Optional[str] = Field(default=None, description=\"Relative path to screenshot\")\n confidence: float = Field(\n default=0.0, description=\"Detection confidence that triggered fallback\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.SourceRecord","title":"<code>SourceRecord</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A content source registered in the knowledge graph for provenance tracking.</p> Source code in <code>video_processor/models.py</code> <pre><code>class SourceRecord(BaseModel):\n \"\"\"A content source registered in the knowledge graph for provenance tracking.\"\"\"\n\n source_id: str = Field(description=\"Unique identifier for this source\")\n source_type: str = Field(description=\"Source type: video, document, url, api, manual\")\n title: str = Field(description=\"Human-readable title\")\n path: Optional[str] = Field(default=None, description=\"Local file path\")\n url: Optional[str] = Field(default=None, description=\"URL if applicable\")\n mime_type: Optional[str] = Field(default=None, description=\"MIME type of the source\")\n ingested_at: str = Field(\n default_factory=lambda: datetime.now().isoformat(),\n description=\"ISO format ingestion timestamp\",\n )\n metadata: Dict[str, Any] = Field(default_factory=dict, description=\"Additional source metadata\")\n</code></pre>"},{"location":"api/models/#video_processor.models.TranscriptSegment","title":"<code>TranscriptSegment</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A single segment of transcribed audio.</p> Source code in <code>video_processor/models.py</code> <pre><code>class TranscriptSegment(BaseModel):\n \"\"\"A single segment of transcribed audio.\"\"\"\n\n start: float = Field(description=\"Start time in seconds\")\n end: float = Field(description=\"End time in seconds\")\n text: str = Field(description=\"Transcribed text\")\n speaker: Optional[str] = Field(default=None, description=\"Speaker identifier\")\n confidence: Optional[float] = Field(default=None, description=\"Transcription confidence 0-1\")\n</code></pre>"},{"location":"api/models/#video_processor.models.VideoManifest","title":"<code>VideoManifest</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Manifest for a single video processing run - the single source of truth.</p> Source code in <code>video_processor/models.py</code> <pre><code>class VideoManifest(BaseModel):\n \"\"\"Manifest for a single video processing run - the single source of truth.\"\"\"\n\n version: str = Field(default=\"1.0\", description=\"Manifest schema version\")\n video: VideoMetadata = Field(description=\"Source video metadata\")\n stats: ProcessingStats = Field(default_factory=ProcessingStats)\n\n # Relative paths to output files\n transcript_json: Optional[str] = Field(default=None)\n transcript_txt: Optional[str] = Field(default=None)\n transcript_srt: Optional[str] = Field(default=None)\n analysis_md: Optional[str] = Field(default=None)\n analysis_html: Optional[str] = Field(default=None)\n analysis_pdf: Optional[str] = Field(default=None)\n knowledge_graph_json: Optional[str] = Field(default=None)\n knowledge_graph_db: Optional[str] = Field(default=None)\n key_points_json: Optional[str] = Field(default=None)\n action_items_json: Optional[str] = Field(default=None)\n\n # Inline structured data\n key_points: List[KeyPoint] = Field(default_factory=list)\n action_items: List[ActionItem] = Field(default_factory=list)\n diagrams: List[DiagramResult] = Field(default_factory=list)\n screen_captures: List[ScreenCapture] = Field(default_factory=list)\n\n # Frame paths\n frame_paths: List[str] = Field(\n default_factory=list, description=\"Relative paths to extracted frames\"\n )\n</code></pre>"},{"location":"api/models/#video_processor.models.VideoMetadata","title":"<code>VideoMetadata</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Metadata about the source video.</p> Source code in <code>video_processor/models.py</code> <pre><code>class VideoMetadata(BaseModel):\n \"\"\"Metadata about the source video.\"\"\"\n\n title: str = Field(description=\"Video title\")\n source_path: Optional[str] = Field(default=None, description=\"Original video file path\")\n duration_seconds: Optional[float] = Field(default=None, description=\"Video duration\")\n resolution: Optional[str] = Field(default=None, description=\"Video resolution (e.g. 1920x1080)\")\n processed_at: str = Field(\n default_factory=lambda: datetime.now().isoformat(),\n description=\"ISO format processing timestamp\",\n )\n</code></pre>"},{"location":"api/models/#overview","title":"Overview","text":"<p>The <code>video_processor.models</code> module defines all Pydantic data models used throughout PlanOpticon for structured output, serialization, and validation. These models represent everything from individual transcript segments to complete batch processing manifests.</p> <p>All models inherit from <code>pydantic.BaseModel</code> and support JSON serialization via <code>.model_dump_json()</code> and deserialization via <code>.model_validate_json()</code>.</p>"},{"location":"api/models/#enumerations","title":"Enumerations","text":""},{"location":"api/models/#diagramtype","title":"DiagramType","text":"<p>Types of visual content detected in video frames.</p> <pre><code>from video_processor.models import DiagramType\n</code></pre> Value Description <code>flowchart</code> Process flow or decision tree diagrams <code>sequence</code> Sequence or interaction diagrams <code>architecture</code> System architecture diagrams <code>whiteboard</code> Whiteboard drawings or sketches <code>chart</code> Data charts (bar, line, pie, scatter) <code>table</code> Tabular data <code>slide</code> Presentation slides <code>screenshot</code> Application screenshots or screen shares <code>unknown</code> Unclassified visual content"},{"location":"api/models/#outputformat","title":"OutputFormat","text":"<p>Available output formats for processing results.</p> Value Description <code>markdown</code> Markdown text <code>json</code> JSON data <code>html</code> HTML document <code>pdf</code> PDF document <code>svg</code> SVG vector graphic <code>png</code> PNG raster image"},{"location":"api/models/#planningentitytype","title":"PlanningEntityType","text":"<p>Classification types for entities in a planning taxonomy.</p> Value Description <code>goal</code> Project goals or objectives <code>requirement</code> Functional or non-functional requirements <code>constraint</code> Limitations or constraints <code>decision</code> Decisions made during planning <code>risk</code> Identified risks <code>assumption</code> Planning assumptions <code>dependency</code> External or internal dependencies <code>milestone</code> Project milestones <code>task</code> Actionable tasks <code>feature</code> Product features"},{"location":"api/models/#planningrelationshiptype","title":"PlanningRelationshipType","text":"<p>Relationship types within a planning taxonomy.</p> Value Description <code>requires</code> Entity A requires entity B <code>blocked_by</code> Entity A is blocked by entity B <code>has_risk</code> Entity A has an associated risk B <code>depends_on</code> Entity A depends on entity B <code>addresses</code> Entity A addresses entity B <code>has_tradeoff</code> Entity A involves a tradeoff with entity B <code>delivers</code> Entity A delivers entity B <code>implements</code> Entity A implements entity B <code>parent_of</code> Entity A is the parent of entity B"},{"location":"api/models/#protocols","title":"Protocols","text":""},{"location":"api/models/#progresscallback","title":"ProgressCallback","text":"<p>A runtime-checkable protocol for receiving pipeline progress updates. Implement this interface to integrate custom progress reporting (e.g., web UI, logging).</p> <pre><code>from video_processor.models import ProgressCallback\n\nclass MyProgress:\n def on_step_start(self, step: str, index: int, total: int) -> None:\n print(f\"Starting {step} ({index}/{total})\")\n\n def on_step_complete(self, step: str, index: int, total: int) -> None:\n print(f\"Completed {step} ({index}/{total})\")\n\n def on_progress(self, step: str, percent: float, message: str = \"\") -> None:\n print(f\"{step}: {percent:.0f}% {message}\")\n\nassert isinstance(MyProgress(), ProgressCallback) # True\n</code></pre> <p>Methods:</p> Method Parameters Description <code>on_step_start</code> <code>step: str</code>, <code>index: int</code>, <code>total: int</code> Called when a pipeline step begins <code>on_step_complete</code> <code>step: str</code>, <code>index: int</code>, <code>total: int</code> Called when a pipeline step finishes <code>on_progress</code> <code>step: str</code>, <code>percent: float</code>, <code>message: str</code> Called with incremental progress updates"},{"location":"api/models/#transcript-models","title":"Transcript Models","text":""},{"location":"api/models/#transcriptsegment","title":"TranscriptSegment","text":"<p>A single segment of transcribed audio with timing and optional speaker identification.</p> Field Type Default Description <code>start</code> <code>float</code> required Start time in seconds <code>end</code> <code>float</code> required End time in seconds <code>text</code> <code>str</code> required Transcribed text content <code>speaker</code> <code>Optional[str]</code> <code>None</code> Speaker identifier (e.g., \"Speaker 1\") <code>confidence</code> <code>Optional[float]</code> <code>None</code> Transcription confidence score (0.0 to 1.0) <pre><code>{\n \"start\": 12.5,\n \"end\": 15.3,\n \"text\": \"We should migrate to the new API by next quarter.\",\n \"speaker\": \"Alice\",\n \"confidence\": 0.95\n}\n</code></pre>"},{"location":"api/models/#content-extraction-models","title":"Content Extraction Models","text":""},{"location":"api/models/#actionitem","title":"ActionItem","text":"<p>An action item extracted from transcript or diagram content.</p> Field Type Default Description <code>action</code> <code>str</code> required The action to be taken <code>assignee</code> <code>Optional[str]</code> <code>None</code> Person responsible for the action <code>deadline</code> <code>Optional[str]</code> <code>None</code> Deadline or timeframe <code>priority</code> <code>Optional[str]</code> <code>None</code> Priority level (e.g., \"high\", \"medium\", \"low\") <code>context</code> <code>Optional[str]</code> <code>None</code> Additional context or notes <code>source</code> <code>Optional[str]</code> <code>None</code> Where this was found: <code>\"transcript\"</code>, <code>\"diagram\"</code>, or <code>\"both\"</code> <pre><code>{\n \"action\": \"Migrate authentication service to OAuth 2.0\",\n \"assignee\": \"Bob\",\n \"deadline\": \"Q2 2026\",\n \"priority\": \"high\",\n \"context\": \"at 245s\",\n \"source\": \"transcript\"\n}\n</code></pre>"},{"location":"api/models/#keypoint","title":"KeyPoint","text":"<p>A key point extracted from content, optionally linked to diagrams.</p> Field Type Default Description <code>point</code> <code>str</code> required The key point text <code>topic</code> <code>Optional[str]</code> <code>None</code> Topic or category <code>details</code> <code>Optional[str]</code> <code>None</code> Supporting details <code>timestamp</code> <code>Optional[float]</code> <code>None</code> Timestamp in video (seconds) <code>source</code> <code>Optional[str]</code> <code>None</code> Where this was found <code>related_diagrams</code> <code>List[int]</code> <code>[]</code> Indices of related diagrams in the manifest <pre><code>{\n \"point\": \"Team decided to use FalkorDB for graph storage\",\n \"topic\": \"Architecture\",\n \"details\": \"Embedded database avoids infrastructure overhead for CLI use\",\n \"timestamp\": 342.0,\n \"source\": \"transcript\",\n \"related_diagrams\": [0, 2]\n}\n</code></pre>"},{"location":"api/models/#diagram-models","title":"Diagram Models","text":""},{"location":"api/models/#diagramresult","title":"DiagramResult","text":"<p>Result from diagram extraction and analysis. Contains structured data extracted from visual content, along with paths to output files.</p> Field Type Default Description <code>frame_index</code> <code>int</code> required Index of the source frame <code>timestamp</code> <code>Optional[float]</code> <code>None</code> Timestamp in video (seconds) <code>diagram_type</code> <code>DiagramType</code> <code>unknown</code> Type of diagram detected <code>confidence</code> <code>float</code> <code>0.0</code> Detection confidence (0.0 to 1.0) <code>description</code> <code>Optional[str]</code> <code>None</code> Detailed description of the diagram <code>text_content</code> <code>Optional[str]</code> <code>None</code> All visible text, preserving structure <code>elements</code> <code>List[str]</code> <code>[]</code> Identified elements or components <code>relationships</code> <code>List[str]</code> <code>[]</code> Identified relationships (e.g., <code>\"A -> B: connects\"</code>) <code>mermaid</code> <code>Optional[str]</code> <code>None</code> Mermaid syntax representation <code>chart_data</code> <code>Optional[Dict[str, Any]]</code> <code>None</code> Extractable chart data (<code>labels</code>, <code>values</code>, <code>chart_type</code>) <code>image_path</code> <code>Optional[str]</code> <code>None</code> Relative path to original frame image <code>svg_path</code> <code>Optional[str]</code> <code>None</code> Relative path to rendered SVG <code>png_path</code> <code>Optional[str]</code> <code>None</code> Relative path to rendered PNG <code>mermaid_path</code> <code>Optional[str]</code> <code>None</code> Relative path to mermaid source file <pre><code>{\n \"frame_index\": 5,\n \"timestamp\": 120.0,\n \"diagram_type\": \"architecture\",\n \"confidence\": 0.92,\n \"description\": \"Microservices architecture showing API gateway, auth service, and database layer\",\n \"text_content\": \"API Gateway\\nAuth Service\\nUser DB\\nPostgreSQL\",\n \"elements\": [\"API Gateway\", \"Auth Service\", \"User DB\", \"PostgreSQL\"],\n \"relationships\": [\"API Gateway -> Auth Service: authenticates\", \"Auth Service -> User DB: queries\"],\n \"mermaid\": \"graph LR\\n A[API Gateway] --> B[Auth Service]\\n B --> C[User DB]\",\n \"chart_data\": null,\n \"image_path\": \"diagrams/diagram_0.jpg\",\n \"svg_path\": null,\n \"png_path\": null,\n \"mermaid_path\": \"diagrams/diagram_0.mermaid\"\n}\n</code></pre>"},{"location":"api/models/#screencapture","title":"ScreenCapture","text":"<p>A screengrab fallback created when diagram extraction fails or confidence is too low for full analysis.</p> Field Type Default Description <code>frame_index</code> <code>int</code> required Index of the source frame <code>timestamp</code> <code>Optional[float]</code> <code>None</code> Timestamp in video (seconds) <code>caption</code> <code>Optional[str]</code> <code>None</code> Brief description of the content <code>image_path</code> <code>Optional[str]</code> <code>None</code> Relative path to screenshot image <code>confidence</code> <code>float</code> <code>0.0</code> Detection confidence that triggered fallback <pre><code>{\n \"frame_index\": 8,\n \"timestamp\": 195.0,\n \"caption\": \"Code editor showing a Python function definition\",\n \"image_path\": \"captures/capture_0.jpg\",\n \"confidence\": 0.45\n}\n</code></pre>"},{"location":"api/models/#knowledge-graph-models","title":"Knowledge Graph Models","text":""},{"location":"api/models/#entity","title":"Entity","text":"<p>An entity in the knowledge graph, representing a person, concept, technology, or other named item extracted from content.</p> Field Type Default Description <code>name</code> <code>str</code> required Entity name <code>type</code> <code>str</code> <code>\"concept\"</code> Entity type: <code>\"person\"</code>, <code>\"concept\"</code>, <code>\"technology\"</code>, <code>\"time\"</code>, <code>\"diagram\"</code> <code>descriptions</code> <code>List[str]</code> <code>[]</code> Accumulated descriptions of this entity <code>source</code> <code>Optional[str]</code> <code>None</code> Source attribution: <code>\"transcript\"</code>, <code>\"diagram\"</code>, or <code>\"both\"</code> <code>occurrences</code> <code>List[Dict[str, Any]]</code> <code>[]</code> Occurrences with source, timestamp, and text context <pre><code>{\n \"name\": \"FalkorDB\",\n \"type\": \"technology\",\n \"descriptions\": [\"Embedded graph database\", \"Supports Cypher queries\"],\n \"source\": \"both\",\n \"occurrences\": [\n {\"source\": \"transcript\", \"timestamp\": 120.0, \"text\": \"We chose FalkorDB for graph storage\"},\n {\"source\": \"diagram\", \"text\": \"FalkorDB Lite\"}\n ]\n}\n</code></pre>"},{"location":"api/models/#relationship","title":"Relationship","text":"<p>A directed relationship between two entities in the knowledge graph.</p> Field Type Default Description <code>source</code> <code>str</code> required Source entity name <code>target</code> <code>str</code> required Target entity name <code>type</code> <code>str</code> <code>\"related_to\"</code> Relationship type (e.g., <code>\"uses\"</code>, <code>\"manages\"</code>, <code>\"related_to\"</code>) <code>content_source</code> <code>Optional[str]</code> <code>None</code> Content source identifier <code>timestamp</code> <code>Optional[float]</code> <code>None</code> Timestamp in seconds <pre><code>{\n \"source\": \"PlanOpticon\",\n \"target\": \"FalkorDB\",\n \"type\": \"uses\",\n \"content_source\": \"transcript\",\n \"timestamp\": 125.0\n}\n</code></pre>"},{"location":"api/models/#sourcerecord","title":"SourceRecord","text":"<p>A content source registered in the knowledge graph for provenance tracking.</p> Field Type Default Description <code>source_id</code> <code>str</code> required Unique identifier for this source <code>source_type</code> <code>str</code> required Source type: <code>\"video\"</code>, <code>\"document\"</code>, <code>\"url\"</code>, <code>\"api\"</code>, <code>\"manual\"</code> <code>title</code> <code>str</code> required Human-readable title <code>path</code> <code>Optional[str]</code> <code>None</code> Local file path <code>url</code> <code>Optional[str]</code> <code>None</code> URL if applicable <code>mime_type</code> <code>Optional[str]</code> <code>None</code> MIME type of the source <code>ingested_at</code> <code>str</code> auto ISO format ingestion timestamp (auto-generated) <code>metadata</code> <code>Dict[str, Any]</code> <code>{}</code> Additional source metadata <pre><code>{\n \"source_id\": \"vid_abc123\",\n \"source_type\": \"video\",\n \"title\": \"Sprint Planning Meeting - Jan 15\",\n \"path\": \"/recordings/sprint-planning.mp4\",\n \"url\": null,\n \"mime_type\": \"video/mp4\",\n \"ingested_at\": \"2026-01-15T10:30:00\",\n \"metadata\": {\"duration\": 3600, \"resolution\": \"1920x1080\"}\n}\n</code></pre>"},{"location":"api/models/#knowledgegraphdata","title":"KnowledgeGraphData","text":"<p>Serializable knowledge graph data containing all nodes, relationships, and source provenance.</p> Field Type Default Description <code>nodes</code> <code>List[Entity]</code> <code>[]</code> Graph nodes/entities <code>relationships</code> <code>List[Relationship]</code> <code>[]</code> Graph relationships <code>sources</code> <code>List[SourceRecord]</code> <code>[]</code> Content sources for provenance tracking"},{"location":"api/models/#planning-models","title":"Planning Models","text":""},{"location":"api/models/#planningentity","title":"PlanningEntity","text":"<p>An entity classified for planning purposes, with priority and status tracking.</p> Field Type Default Description <code>name</code> <code>str</code> required Entity name <code>planning_type</code> <code>PlanningEntityType</code> required Planning classification <code>description</code> <code>str</code> <code>\"\"</code> Detailed description <code>priority</code> <code>Optional[str]</code> <code>None</code> Priority: <code>\"high\"</code>, <code>\"medium\"</code>, <code>\"low\"</code> <code>status</code> <code>Optional[str]</code> <code>None</code> Status: <code>\"identified\"</code>, <code>\"confirmed\"</code>, <code>\"resolved\"</code> <code>source_entities</code> <code>List[str]</code> <code>[]</code> Names of source KG entities this was derived from <code>metadata</code> <code>Dict[str, Any]</code> <code>{}</code> Additional metadata <pre><code>{\n \"name\": \"Migrate to OAuth 2.0\",\n \"planning_type\": \"task\",\n \"description\": \"Replace custom auth with OAuth 2.0 across all services\",\n \"priority\": \"high\",\n \"status\": \"identified\",\n \"source_entities\": [\"OAuth\", \"Authentication Service\"],\n \"metadata\": {}\n}\n</code></pre>"},{"location":"api/models/#processing-and-metadata-models","title":"Processing and Metadata Models","text":""},{"location":"api/models/#processingstats","title":"ProcessingStats","text":"<p>Statistics about a processing run, including model usage tracking.</p> Field Type Default Description <code>start_time</code> <code>Optional[str]</code> <code>None</code> ISO format start time <code>end_time</code> <code>Optional[str]</code> <code>None</code> ISO format end time <code>duration_seconds</code> <code>Optional[float]</code> <code>None</code> Total processing time <code>frames_extracted</code> <code>int</code> <code>0</code> Number of frames extracted from video <code>people_frames_filtered</code> <code>int</code> <code>0</code> Frames filtered out (contained people/webcam) <code>diagrams_detected</code> <code>int</code> <code>0</code> Number of diagrams detected <code>screen_captures</code> <code>int</code> <code>0</code> Number of screen captures saved <code>transcript_duration_seconds</code> <code>Optional[float]</code> <code>None</code> Duration of transcribed audio <code>models_used</code> <code>Dict[str, str]</code> <code>{}</code> Map of task to model used (e.g., <code>{\"vision\": \"gpt-4o\"}</code>)"},{"location":"api/models/#videometadata","title":"VideoMetadata","text":"<p>Metadata about the source video file.</p> Field Type Default Description <code>title</code> <code>str</code> required Video title <code>source_path</code> <code>Optional[str]</code> <code>None</code> Original video file path <code>duration_seconds</code> <code>Optional[float]</code> <code>None</code> Video duration in seconds <code>resolution</code> <code>Optional[str]</code> <code>None</code> Video resolution (e.g., <code>\"1920x1080\"</code>) <code>processed_at</code> <code>str</code> auto ISO format processing timestamp"},{"location":"api/models/#manifest-models","title":"Manifest Models","text":""},{"location":"api/models/#videomanifest","title":"VideoManifest","text":"<p>The single source of truth for a video processing run. Contains all output paths, inline structured data, and processing statistics.</p> Field Type Default Description <code>version</code> <code>str</code> <code>\"1.0\"</code> Manifest schema version <code>video</code> <code>VideoMetadata</code> required Source video metadata <code>stats</code> <code>ProcessingStats</code> default Processing statistics <code>transcript_json</code> <code>Optional[str]</code> <code>None</code> Relative path to transcript JSON <code>transcript_txt</code> <code>Optional[str]</code> <code>None</code> Relative path to transcript text <code>transcript_srt</code> <code>Optional[str]</code> <code>None</code> Relative path to SRT subtitles <code>analysis_md</code> <code>Optional[str]</code> <code>None</code> Relative path to analysis Markdown <code>analysis_html</code> <code>Optional[str]</code> <code>None</code> Relative path to analysis HTML <code>analysis_pdf</code> <code>Optional[str]</code> <code>None</code> Relative path to analysis PDF <code>knowledge_graph_json</code> <code>Optional[str]</code> <code>None</code> Relative path to knowledge graph JSON <code>knowledge_graph_db</code> <code>Optional[str]</code> <code>None</code> Relative path to knowledge graph DB <code>key_points_json</code> <code>Optional[str]</code> <code>None</code> Relative path to key points JSON <code>action_items_json</code> <code>Optional[str]</code> <code>None</code> Relative path to action items JSON <code>key_points</code> <code>List[KeyPoint]</code> <code>[]</code> Inline key points data <code>action_items</code> <code>List[ActionItem]</code> <code>[]</code> Inline action items data <code>diagrams</code> <code>List[DiagramResult]</code> <code>[]</code> Inline diagram results <code>screen_captures</code> <code>List[ScreenCapture]</code> <code>[]</code> Inline screen captures <code>frame_paths</code> <code>List[str]</code> <code>[]</code> Relative paths to extracted frames <pre><code>from video_processor.models import VideoManifest, VideoMetadata\n\nmanifest = VideoManifest(\n video=VideoMetadata(title=\"Sprint Planning\"),\n key_points=[...],\n action_items=[...],\n diagrams=[...],\n)\n\n# Serialize to JSON\nmanifest.model_dump_json(indent=2)\n\n# Load from file\nloaded = VideoManifest.model_validate_json(Path(\"manifest.json\").read_text())\n</code></pre>"},{"location":"api/models/#batchvideoentry","title":"BatchVideoEntry","text":"<p>Summary of a single video within a batch processing run.</p> Field Type Default Description <code>video_name</code> <code>str</code> required Video file name <code>manifest_path</code> <code>str</code> required Relative path to the video's manifest file <code>status</code> <code>str</code> <code>\"pending\"</code> Processing status: <code>\"pending\"</code>, <code>\"completed\"</code>, <code>\"failed\"</code> <code>error</code> <code>Optional[str]</code> <code>None</code> Error message if processing failed <code>diagrams_count</code> <code>int</code> <code>0</code> Number of diagrams detected <code>action_items_count</code> <code>int</code> <code>0</code> Number of action items extracted <code>key_points_count</code> <code>int</code> <code>0</code> Number of key points extracted <code>duration_seconds</code> <code>Optional[float]</code> <code>None</code> Processing duration"},{"location":"api/models/#batchmanifest","title":"BatchManifest","text":"<p>Manifest for a batch processing run across multiple videos.</p> Field Type Default Description <code>version</code> <code>str</code> <code>\"1.0\"</code> Manifest schema version <code>title</code> <code>str</code> <code>\"Batch Processing Results\"</code> Batch title <code>processed_at</code> <code>str</code> auto ISO format timestamp <code>stats</code> <code>ProcessingStats</code> default Aggregated processing statistics <code>videos</code> <code>List[BatchVideoEntry]</code> <code>[]</code> Per-video summaries <code>total_videos</code> <code>int</code> <code>0</code> Total number of videos in batch <code>completed_videos</code> <code>int</code> <code>0</code> Successfully processed videos <code>failed_videos</code> <code>int</code> <code>0</code> Videos that failed processing <code>total_diagrams</code> <code>int</code> <code>0</code> Total diagrams across all videos <code>total_action_items</code> <code>int</code> <code>0</code> Total action items across all videos <code>total_key_points</code> <code>int</code> <code>0</code> Total key points across all videos <code>batch_summary_md</code> <code>Optional[str]</code> <code>None</code> Relative path to batch summary Markdown <code>merged_knowledge_graph_json</code> <code>Optional[str]</code> <code>None</code> Relative path to merged KG JSON <code>merged_knowledge_graph_db</code> <code>Optional[str]</code> <code>None</code> Relative path to merged KG database <pre><code>from video_processor.models import BatchManifest\n\nbatch = BatchManifest(\n title=\"Weekly Recordings\",\n total_videos=5,\n completed_videos=4,\n failed_videos=1,\n)\n</code></pre>"},{"location":"api/providers/","title":"Providers API Reference","text":""},{"location":"api/providers/#video_processor.providers.base","title":"<code>video_processor.providers.base</code>","text":"<p>Abstract base class, registry, and shared types for provider implementations.</p>"},{"location":"api/providers/#video_processor.providers.base.BaseProvider","title":"<code>BaseProvider</code>","text":"<p> Bases: <code>ABC</code></p> <p>Abstract base for all provider implementations.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>class BaseProvider(ABC):\n \"\"\"Abstract base for all provider implementations.\"\"\"\n\n provider_name: str = \"\"\n\n @abstractmethod\n def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n model: Optional[str] = None,\n ) -> str:\n \"\"\"Send a chat completion request. Returns the assistant text.\"\"\"\n\n @abstractmethod\n def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n model: Optional[str] = None,\n ) -> str:\n \"\"\"Analyze an image with a prompt. Returns the assistant text.\"\"\"\n\n @abstractmethod\n def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n model: Optional[str] = None,\n ) -> dict:\n \"\"\"Transcribe an audio file. Returns dict with 'text', 'segments', etc.\"\"\"\n\n @abstractmethod\n def list_models(self) -> list[ModelInfo]:\n \"\"\"Discover available models from this provider's API.\"\"\"\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.BaseProvider.analyze_image","title":"<code>analyze_image(image_bytes, prompt, max_tokens=4096, model=None)</code> <code>abstractmethod</code>","text":"<p>Analyze an image with a prompt. Returns the assistant text.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@abstractmethod\ndef analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n model: Optional[str] = None,\n) -> str:\n \"\"\"Analyze an image with a prompt. Returns the assistant text.\"\"\"\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.BaseProvider.chat","title":"<code>chat(messages, max_tokens=4096, temperature=0.7, model=None)</code> <code>abstractmethod</code>","text":"<p>Send a chat completion request. Returns the assistant text.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@abstractmethod\ndef chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n model: Optional[str] = None,\n) -> str:\n \"\"\"Send a chat completion request. Returns the assistant text.\"\"\"\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.BaseProvider.list_models","title":"<code>list_models()</code> <code>abstractmethod</code>","text":"<p>Discover available models from this provider's API.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@abstractmethod\ndef list_models(self) -> list[ModelInfo]:\n \"\"\"Discover available models from this provider's API.\"\"\"\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.BaseProvider.transcribe_audio","title":"<code>transcribe_audio(audio_path, language=None, model=None)</code> <code>abstractmethod</code>","text":"<p>Transcribe an audio file. Returns dict with 'text', 'segments', etc.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@abstractmethod\ndef transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n model: Optional[str] = None,\n) -> dict:\n \"\"\"Transcribe an audio file. Returns dict with 'text', 'segments', etc.\"\"\"\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ModelInfo","title":"<code>ModelInfo</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>Information about an available model.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>class ModelInfo(BaseModel):\n \"\"\"Information about an available model.\"\"\"\n\n id: str = Field(description=\"Model identifier (e.g. gpt-4o)\")\n provider: str = Field(description=\"Provider name (openai, anthropic, gemini)\")\n display_name: str = Field(default=\"\", description=\"Human-readable name\")\n capabilities: List[str] = Field(\n default_factory=list, description=\"Model capabilities: chat, vision, audio, embedding\"\n )\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.OpenAICompatibleProvider","title":"<code>OpenAICompatibleProvider</code>","text":"<p> Bases: <code>BaseProvider</code></p> <p>Base for providers using OpenAI-compatible APIs.</p> <p>Suitable for Together, Fireworks, Cerebras, xAI, Azure, and similar services.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>class OpenAICompatibleProvider(BaseProvider):\n \"\"\"Base for providers using OpenAI-compatible APIs.\n\n Suitable for Together, Fireworks, Cerebras, xAI, Azure, and similar services.\n \"\"\"\n\n provider_name: str = \"\"\n base_url: str = \"\"\n env_var: str = \"\"\n\n def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None):\n from openai import OpenAI\n\n self._api_key = api_key or os.getenv(self.env_var, \"\")\n self._base_url = base_url or self.base_url\n self._client = OpenAI(api_key=self._api_key, base_url=self._base_url)\n self._last_usage = None\n\n def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n model: Optional[str] = None,\n ) -> str:\n model = model or \"gpt-4o\"\n response = self._client.chat.completions.create(\n model=model,\n messages=messages,\n max_tokens=max_tokens,\n temperature=temperature,\n )\n self._last_usage = {\n \"input_tokens\": getattr(response.usage, \"prompt_tokens\", 0) if response.usage else 0,\n \"output_tokens\": getattr(response.usage, \"completion_tokens\", 0)\n if response.usage\n else 0,\n }\n return response.choices[0].message.content or \"\"\n\n def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n model: Optional[str] = None,\n ) -> str:\n model = model or \"gpt-4o\"\n b64 = base64.b64encode(image_bytes).decode()\n response = self._client.chat.completions.create(\n model=model,\n messages=[\n {\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": prompt},\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": f\"data:image/jpeg;base64,{b64}\"},\n },\n ],\n }\n ],\n max_tokens=max_tokens,\n )\n self._last_usage = {\n \"input_tokens\": getattr(response.usage, \"prompt_tokens\", 0) if response.usage else 0,\n \"output_tokens\": getattr(response.usage, \"completion_tokens\", 0)\n if response.usage\n else 0,\n }\n return response.choices[0].message.content or \"\"\n\n def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n model: Optional[str] = None,\n ) -> dict:\n raise NotImplementedError(f\"{self.provider_name} does not support audio transcription\")\n\n def list_models(self) -> list[ModelInfo]:\n models = []\n try:\n for m in self._client.models.list():\n mid = m.id\n caps = [\"chat\"]\n models.append(\n ModelInfo(\n id=mid,\n provider=self.provider_name,\n display_name=mid,\n capabilities=caps,\n )\n )\n except Exception as e:\n logger.warning(f\"Failed to list {self.provider_name} models: {e}\")\n return sorted(models, key=lambda m: m.id)\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry","title":"<code>ProviderRegistry</code>","text":"<p>Registry for provider classes. Providers register themselves with metadata.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>class ProviderRegistry:\n \"\"\"Registry for provider classes. Providers register themselves with metadata.\"\"\"\n\n _providers: Dict[str, Dict] = {}\n\n @classmethod\n def register(\n cls,\n name: str,\n provider_class: type,\n env_var: str = \"\",\n model_prefixes: Optional[List[str]] = None,\n default_models: Optional[Dict[str, str]] = None,\n ) -> None:\n \"\"\"Register a provider class with its metadata.\"\"\"\n cls._providers[name] = {\n \"class\": provider_class,\n \"env_var\": env_var,\n \"model_prefixes\": model_prefixes or [],\n \"default_models\": default_models or {},\n }\n\n @classmethod\n def get(cls, name: str) -> type:\n \"\"\"Return the provider class for a given name.\"\"\"\n if name not in cls._providers:\n raise ValueError(f\"Unknown provider: {name}\")\n return cls._providers[name][\"class\"]\n\n @classmethod\n def get_by_model(cls, model_id: str) -> Optional[str]:\n \"\"\"Return provider name for a model ID based on prefix matching.\"\"\"\n for name, info in cls._providers.items():\n for prefix in info[\"model_prefixes\"]:\n if model_id.startswith(prefix):\n return name\n return None\n\n @classmethod\n def get_default_models(cls, name: str) -> Dict[str, str]:\n \"\"\"Return the default models dict for a provider.\"\"\"\n if name not in cls._providers:\n return {}\n return cls._providers[name].get(\"default_models\", {})\n\n @classmethod\n def available(cls) -> List[str]:\n \"\"\"Return names of providers whose env var is set (or have no env var requirement).\"\"\"\n result = []\n for name, info in cls._providers.items():\n env_var = info.get(\"env_var\", \"\")\n if not env_var:\n # Providers without an env var (e.g. ollama) need special availability checks\n result.append(name)\n elif os.getenv(env_var, \"\"):\n result.append(name)\n return result\n\n @classmethod\n def all_registered(cls) -> Dict[str, Dict]:\n \"\"\"Return all registered providers and their metadata.\"\"\"\n return dict(cls._providers)\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.all_registered","title":"<code>all_registered()</code> <code>classmethod</code>","text":"<p>Return all registered providers and their metadata.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef all_registered(cls) -> Dict[str, Dict]:\n \"\"\"Return all registered providers and their metadata.\"\"\"\n return dict(cls._providers)\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.available","title":"<code>available()</code> <code>classmethod</code>","text":"<p>Return names of providers whose env var is set (or have no env var requirement).</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef available(cls) -> List[str]:\n \"\"\"Return names of providers whose env var is set (or have no env var requirement).\"\"\"\n result = []\n for name, info in cls._providers.items():\n env_var = info.get(\"env_var\", \"\")\n if not env_var:\n # Providers without an env var (e.g. ollama) need special availability checks\n result.append(name)\n elif os.getenv(env_var, \"\"):\n result.append(name)\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.get","title":"<code>get(name)</code> <code>classmethod</code>","text":"<p>Return the provider class for a given name.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef get(cls, name: str) -> type:\n \"\"\"Return the provider class for a given name.\"\"\"\n if name not in cls._providers:\n raise ValueError(f\"Unknown provider: {name}\")\n return cls._providers[name][\"class\"]\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.get_by_model","title":"<code>get_by_model(model_id)</code> <code>classmethod</code>","text":"<p>Return provider name for a model ID based on prefix matching.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef get_by_model(cls, model_id: str) -> Optional[str]:\n \"\"\"Return provider name for a model ID based on prefix matching.\"\"\"\n for name, info in cls._providers.items():\n for prefix in info[\"model_prefixes\"]:\n if model_id.startswith(prefix):\n return name\n return None\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.get_default_models","title":"<code>get_default_models(name)</code> <code>classmethod</code>","text":"<p>Return the default models dict for a provider.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef get_default_models(cls, name: str) -> Dict[str, str]:\n \"\"\"Return the default models dict for a provider.\"\"\"\n if name not in cls._providers:\n return {}\n return cls._providers[name].get(\"default_models\", {})\n</code></pre>"},{"location":"api/providers/#video_processor.providers.base.ProviderRegistry.register","title":"<code>register(name, provider_class, env_var='', model_prefixes=None, default_models=None)</code> <code>classmethod</code>","text":"<p>Register a provider class with its metadata.</p> Source code in <code>video_processor/providers/base.py</code> <pre><code>@classmethod\ndef register(\n cls,\n name: str,\n provider_class: type,\n env_var: str = \"\",\n model_prefixes: Optional[List[str]] = None,\n default_models: Optional[Dict[str, str]] = None,\n) -> None:\n \"\"\"Register a provider class with its metadata.\"\"\"\n cls._providers[name] = {\n \"class\": provider_class,\n \"env_var\": env_var,\n \"model_prefixes\": model_prefixes or [],\n \"default_models\": default_models or {},\n }\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager","title":"<code>video_processor.providers.manager</code>","text":"<p>ProviderManager - unified interface for routing API calls to the best available provider.</p>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager","title":"<code>ProviderManager</code>","text":"<p>Routes API calls to the best available provider/model.</p> <p>Supports explicit model selection or auto-routing based on discovered available models.</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>class ProviderManager:\n \"\"\"\n Routes API calls to the best available provider/model.\n\n Supports explicit model selection or auto-routing based on\n discovered available models.\n \"\"\"\n\n def __init__(\n self,\n vision_model: Optional[str] = None,\n chat_model: Optional[str] = None,\n transcription_model: Optional[str] = None,\n provider: Optional[str] = None,\n auto: bool = True,\n ):\n \"\"\"\n Initialize the ProviderManager.\n\n Parameters\n ----------\n vision_model : override model for vision tasks (e.g. 'gpt-4o')\n chat_model : override model for chat/LLM tasks\n transcription_model : override model for transcription\n provider : force all tasks to a single provider ('openai', 'anthropic', 'gemini')\n auto : if True and no model specified, pick the best available\n \"\"\"\n _ensure_providers_registered()\n self.auto = auto\n self._providers: dict[str, BaseProvider] = {}\n self._available_models: Optional[list[ModelInfo]] = None\n self.usage = UsageTracker()\n\n # If a single provider is forced, apply it\n if provider:\n self.vision_model = vision_model or self._default_for_provider(provider, \"vision\")\n self.chat_model = chat_model or self._default_for_provider(provider, \"chat\")\n self.transcription_model = transcription_model or self._default_for_provider(\n provider, \"audio\"\n )\n else:\n self.vision_model = vision_model\n self.chat_model = chat_model\n self.transcription_model = transcription_model\n\n self._forced_provider = provider\n\n @staticmethod\n def _default_for_provider(provider: str, capability: str) -> str:\n \"\"\"Return the default model for a provider/capability combo.\"\"\"\n defaults = ProviderRegistry.get_default_models(provider)\n if defaults:\n return defaults.get(capability, \"\")\n # Fallback for unregistered providers\n return \"\"\n\n def _get_provider(self, provider_name: str) -> BaseProvider:\n \"\"\"Lazily initialize and cache a provider instance.\"\"\"\n if provider_name not in self._providers:\n _ensure_providers_registered()\n provider_class = ProviderRegistry.get(provider_name)\n self._providers[provider_name] = provider_class()\n return self._providers[provider_name]\n\n def _provider_for_model(self, model_id: str) -> str:\n \"\"\"Infer the provider from a model id.\"\"\"\n _ensure_providers_registered()\n # Check registry prefix matching first\n provider_name = ProviderRegistry.get_by_model(model_id)\n if provider_name:\n return provider_name\n # Try discovery (exact match, then prefix match for ollama name:tag format)\n models = self._get_available_models()\n for m in models:\n if m.id == model_id:\n return m.provider\n for m in models:\n if m.id.startswith(model_id + \":\"):\n return m.provider\n raise ValueError(f\"Cannot determine provider for model: {model_id}\")\n\n def _get_available_models(self) -> list[ModelInfo]:\n if self._available_models is None:\n self._available_models = discover_available_models()\n return self._available_models\n\n def _resolve_model(\n self, explicit: Optional[str], capability: str, preferences: list[tuple[str, str]]\n ) -> tuple[str, str]:\n \"\"\"\n Resolve which (provider, model) to use for a capability.\n\n Returns (provider_name, model_id).\n \"\"\"\n if explicit:\n prov = self._provider_for_model(explicit)\n return prov, explicit\n\n if self.auto:\n # Try preference order, picking the first provider that has an API key\n for prov, model in preferences:\n try:\n self._get_provider(prov)\n return prov, model\n except (ValueError, ImportError):\n continue\n\n # Fallback: try Ollama if available (no API key needed)\n try:\n from video_processor.providers.ollama_provider import OllamaProvider\n\n if OllamaProvider.is_available():\n provider = self._get_provider(\"ollama\")\n models = provider.list_models()\n for m in models:\n if capability in m.capabilities:\n return \"ollama\", m.id\n except Exception:\n pass\n\n raise RuntimeError(\n f\"No provider available for capability '{capability}'. \"\n \"Set an API key for at least one provider, or start Ollama.\"\n )\n\n def _track(self, provider: BaseProvider, prov_name: str, model: str) -> None:\n \"\"\"Record usage from the last API call on a provider.\"\"\"\n last = getattr(provider, \"_last_usage\", None)\n if last:\n self.usage.record(\n provider=prov_name,\n model=model,\n input_tokens=last.get(\"input_tokens\", 0),\n output_tokens=last.get(\"output_tokens\", 0),\n )\n provider._last_usage = None\n\n # --- Public API ---\n\n def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n ) -> str:\n \"\"\"Send a chat completion to the best available provider.\"\"\"\n prov_name, model = self._resolve_model(self.chat_model, \"chat\", _CHAT_PREFERENCES)\n logger.info(f\"Chat: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n result = provider.chat(\n messages, max_tokens=max_tokens, temperature=temperature, model=model\n )\n self._track(provider, prov_name, model)\n return result\n\n def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n ) -> str:\n \"\"\"Analyze an image using the best available vision provider.\"\"\"\n prov_name, model = self._resolve_model(self.vision_model, \"vision\", _VISION_PREFERENCES)\n logger.info(f\"Vision: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n result = provider.analyze_image(image_bytes, prompt, max_tokens=max_tokens, model=model)\n self._track(provider, prov_name, model)\n return result\n\n def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n speaker_hints: Optional[list[str]] = None,\n ) -> dict:\n \"\"\"Transcribe audio using local Whisper if available, otherwise API.\"\"\"\n # Prefer local Whisper \u2014 no file size limits, no API costs\n if not self.transcription_model or self.transcription_model.startswith(\"whisper-local\"):\n try:\n from video_processor.providers.whisper_local import WhisperLocal\n\n if WhisperLocal.is_available():\n # Parse model size from \"whisper-local:large\" or default to \"large\"\n size = \"large\"\n if self.transcription_model and \":\" in self.transcription_model:\n size = self.transcription_model.split(\":\", 1)[1]\n if not hasattr(self, \"_whisper_local\"):\n self._whisper_local = WhisperLocal(model_size=size)\n logger.info(f\"Transcription: using local whisper-{size}\")\n # Pass speaker names as initial prompt hint for Whisper\n whisper_kwargs = {\"language\": language}\n if speaker_hints:\n whisper_kwargs[\"initial_prompt\"] = (\n \"Speakers: \" + \", \".join(speaker_hints) + \".\"\n )\n result = self._whisper_local.transcribe(audio_path, **whisper_kwargs)\n duration = result.get(\"duration\") or 0\n self.usage.record(\n provider=\"local\",\n model=f\"whisper-{size}\",\n audio_minutes=duration / 60 if duration else 0,\n )\n return result\n except ImportError:\n pass\n\n # Fall back to API-based transcription\n prov_name, model = self._resolve_model(\n self.transcription_model, \"audio\", _TRANSCRIPTION_PREFERENCES\n )\n logger.info(f\"Transcription: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n # Build transcription kwargs, passing speaker hints where supported\n transcribe_kwargs: dict = {\"language\": language, \"model\": model}\n if speaker_hints:\n if prov_name == \"openai\":\n # OpenAI Whisper supports a 'prompt' parameter for hints\n transcribe_kwargs[\"prompt\"] = \"Speakers: \" + \", \".join(speaker_hints) + \".\"\n else:\n transcribe_kwargs[\"speaker_hints\"] = speaker_hints\n result = provider.transcribe_audio(audio_path, **transcribe_kwargs)\n duration = result.get(\"duration\") or 0\n self.usage.record(\n provider=prov_name,\n model=model,\n audio_minutes=duration / 60 if duration else 0,\n )\n return result\n\n def get_models_used(self) -> dict[str, str]:\n \"\"\"Return a dict mapping capability to 'provider/model' for tracking.\"\"\"\n result = {}\n for cap, explicit, prefs in [\n (\"vision\", self.vision_model, _VISION_PREFERENCES),\n (\"chat\", self.chat_model, _CHAT_PREFERENCES),\n (\"transcription\", self.transcription_model, _TRANSCRIPTION_PREFERENCES),\n ]:\n try:\n prov, model = self._resolve_model(explicit, cap, prefs)\n result[cap] = f\"{prov}/{model}\"\n except RuntimeError:\n pass\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.__init__","title":"<code>__init__(vision_model=None, chat_model=None, transcription_model=None, provider=None, auto=True)</code>","text":"<p>Initialize the ProviderManager.</p>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.__init__--parameters","title":"Parameters","text":"<p>vision_model : override model for vision tasks (e.g. 'gpt-4o') chat_model : override model for chat/LLM tasks transcription_model : override model for transcription provider : force all tasks to a single provider ('openai', 'anthropic', 'gemini') auto : if True and no model specified, pick the best available</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>def __init__(\n self,\n vision_model: Optional[str] = None,\n chat_model: Optional[str] = None,\n transcription_model: Optional[str] = None,\n provider: Optional[str] = None,\n auto: bool = True,\n):\n \"\"\"\n Initialize the ProviderManager.\n\n Parameters\n ----------\n vision_model : override model for vision tasks (e.g. 'gpt-4o')\n chat_model : override model for chat/LLM tasks\n transcription_model : override model for transcription\n provider : force all tasks to a single provider ('openai', 'anthropic', 'gemini')\n auto : if True and no model specified, pick the best available\n \"\"\"\n _ensure_providers_registered()\n self.auto = auto\n self._providers: dict[str, BaseProvider] = {}\n self._available_models: Optional[list[ModelInfo]] = None\n self.usage = UsageTracker()\n\n # If a single provider is forced, apply it\n if provider:\n self.vision_model = vision_model or self._default_for_provider(provider, \"vision\")\n self.chat_model = chat_model or self._default_for_provider(provider, \"chat\")\n self.transcription_model = transcription_model or self._default_for_provider(\n provider, \"audio\"\n )\n else:\n self.vision_model = vision_model\n self.chat_model = chat_model\n self.transcription_model = transcription_model\n\n self._forced_provider = provider\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.analyze_image","title":"<code>analyze_image(image_bytes, prompt, max_tokens=4096)</code>","text":"<p>Analyze an image using the best available vision provider.</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n) -> str:\n \"\"\"Analyze an image using the best available vision provider.\"\"\"\n prov_name, model = self._resolve_model(self.vision_model, \"vision\", _VISION_PREFERENCES)\n logger.info(f\"Vision: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n result = provider.analyze_image(image_bytes, prompt, max_tokens=max_tokens, model=model)\n self._track(provider, prov_name, model)\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.chat","title":"<code>chat(messages, max_tokens=4096, temperature=0.7)</code>","text":"<p>Send a chat completion to the best available provider.</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n) -> str:\n \"\"\"Send a chat completion to the best available provider.\"\"\"\n prov_name, model = self._resolve_model(self.chat_model, \"chat\", _CHAT_PREFERENCES)\n logger.info(f\"Chat: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n result = provider.chat(\n messages, max_tokens=max_tokens, temperature=temperature, model=model\n )\n self._track(provider, prov_name, model)\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.get_models_used","title":"<code>get_models_used()</code>","text":"<p>Return a dict mapping capability to 'provider/model' for tracking.</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>def get_models_used(self) -> dict[str, str]:\n \"\"\"Return a dict mapping capability to 'provider/model' for tracking.\"\"\"\n result = {}\n for cap, explicit, prefs in [\n (\"vision\", self.vision_model, _VISION_PREFERENCES),\n (\"chat\", self.chat_model, _CHAT_PREFERENCES),\n (\"transcription\", self.transcription_model, _TRANSCRIPTION_PREFERENCES),\n ]:\n try:\n prov, model = self._resolve_model(explicit, cap, prefs)\n result[cap] = f\"{prov}/{model}\"\n except RuntimeError:\n pass\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.manager.ProviderManager.transcribe_audio","title":"<code>transcribe_audio(audio_path, language=None, speaker_hints=None)</code>","text":"<p>Transcribe audio using local Whisper if available, otherwise API.</p> Source code in <code>video_processor/providers/manager.py</code> <pre><code>def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n speaker_hints: Optional[list[str]] = None,\n) -> dict:\n \"\"\"Transcribe audio using local Whisper if available, otherwise API.\"\"\"\n # Prefer local Whisper \u2014 no file size limits, no API costs\n if not self.transcription_model or self.transcription_model.startswith(\"whisper-local\"):\n try:\n from video_processor.providers.whisper_local import WhisperLocal\n\n if WhisperLocal.is_available():\n # Parse model size from \"whisper-local:large\" or default to \"large\"\n size = \"large\"\n if self.transcription_model and \":\" in self.transcription_model:\n size = self.transcription_model.split(\":\", 1)[1]\n if not hasattr(self, \"_whisper_local\"):\n self._whisper_local = WhisperLocal(model_size=size)\n logger.info(f\"Transcription: using local whisper-{size}\")\n # Pass speaker names as initial prompt hint for Whisper\n whisper_kwargs = {\"language\": language}\n if speaker_hints:\n whisper_kwargs[\"initial_prompt\"] = (\n \"Speakers: \" + \", \".join(speaker_hints) + \".\"\n )\n result = self._whisper_local.transcribe(audio_path, **whisper_kwargs)\n duration = result.get(\"duration\") or 0\n self.usage.record(\n provider=\"local\",\n model=f\"whisper-{size}\",\n audio_minutes=duration / 60 if duration else 0,\n )\n return result\n except ImportError:\n pass\n\n # Fall back to API-based transcription\n prov_name, model = self._resolve_model(\n self.transcription_model, \"audio\", _TRANSCRIPTION_PREFERENCES\n )\n logger.info(f\"Transcription: using {prov_name}/{model}\")\n provider = self._get_provider(prov_name)\n # Build transcription kwargs, passing speaker hints where supported\n transcribe_kwargs: dict = {\"language\": language, \"model\": model}\n if speaker_hints:\n if prov_name == \"openai\":\n # OpenAI Whisper supports a 'prompt' parameter for hints\n transcribe_kwargs[\"prompt\"] = \"Speakers: \" + \", \".join(speaker_hints) + \".\"\n else:\n transcribe_kwargs[\"speaker_hints\"] = speaker_hints\n result = provider.transcribe_audio(audio_path, **transcribe_kwargs)\n duration = result.get(\"duration\") or 0\n self.usage.record(\n provider=prov_name,\n model=model,\n audio_minutes=duration / 60 if duration else 0,\n )\n return result\n</code></pre>"},{"location":"api/providers/#video_processor.providers.discovery","title":"<code>video_processor.providers.discovery</code>","text":"<p>Auto-discover available models across providers.</p>"},{"location":"api/providers/#video_processor.providers.discovery.clear_discovery_cache","title":"<code>clear_discovery_cache()</code>","text":"<p>Clear the cached model list.</p> Source code in <code>video_processor/providers/discovery.py</code> <pre><code>def clear_discovery_cache() -> None:\n \"\"\"Clear the cached model list.\"\"\"\n global _cached_models\n _cached_models = None\n</code></pre>"},{"location":"api/providers/#video_processor.providers.discovery.discover_available_models","title":"<code>discover_available_models(api_keys=None, force_refresh=False)</code>","text":"<p>Discover available models from all configured providers.</p> <p>For each provider with a valid API key, calls list_models() and returns a unified list. Results are cached for the session.</p> Source code in <code>video_processor/providers/discovery.py</code> <pre><code>def discover_available_models(\n api_keys: Optional[dict[str, str]] = None,\n force_refresh: bool = False,\n) -> list[ModelInfo]:\n \"\"\"\n Discover available models from all configured providers.\n\n For each provider with a valid API key, calls list_models() and returns\n a unified list. Results are cached for the session.\n \"\"\"\n global _cached_models\n if _cached_models is not None and not force_refresh:\n return _cached_models\n\n _ensure_providers_registered()\n\n keys = api_keys or {\n \"openai\": os.getenv(\"OPENAI_API_KEY\", \"\"),\n \"anthropic\": os.getenv(\"ANTHROPIC_API_KEY\", \"\"),\n \"gemini\": os.getenv(\"GEMINI_API_KEY\", \"\"),\n }\n\n all_models: list[ModelInfo] = []\n\n for name, info in ProviderRegistry.all_registered().items():\n env_var = info.get(\"env_var\", \"\")\n provider_class = info[\"class\"]\n\n if name == \"ollama\":\n # Ollama: no API key, check server availability\n try:\n if provider_class.is_available():\n provider = provider_class()\n models = provider.list_models()\n logger.info(f\"Discovered {len(models)} Ollama models\")\n all_models.extend(models)\n except Exception as e:\n logger.info(f\"Ollama discovery skipped: {e}\")\n continue\n\n # For key-based providers, check the api_keys dict first, then env var\n key = keys.get(name, \"\")\n if not key and env_var:\n key = os.getenv(env_var, \"\")\n\n # Special case: Gemini also supports service account credentials\n gemini_creds = \"\"\n if name == \"gemini\":\n gemini_creds = os.getenv(\"GOOGLE_APPLICATION_CREDENTIALS\", \"\")\n\n if not key and not gemini_creds:\n continue\n\n try:\n # Handle provider-specific constructor args\n if name == \"gemini\":\n provider = provider_class(\n api_key=key or None,\n credentials_path=gemini_creds or None,\n )\n else:\n provider = provider_class(api_key=key)\n models = provider.list_models()\n logger.info(f\"Discovered {len(models)} {name.capitalize()} models\")\n all_models.extend(models)\n except Exception as e:\n logger.info(f\"{name.capitalize()} discovery skipped: {e}\")\n\n # Sort by provider then id\n all_models.sort(key=lambda m: (m.provider, m.id))\n _cached_models = all_models\n logger.info(f\"Total discovered models: {len(all_models)}\")\n return all_models\n</code></pre>"},{"location":"api/providers/#overview","title":"Overview","text":"<p>The provider system abstracts LLM API calls behind a unified interface. It supports multiple providers (OpenAI, Anthropic, Gemini, Ollama, and OpenAI-compatible services), automatic model discovery, capability-based routing, and usage tracking.</p> <p>Key components:</p> <ul> <li><code>BaseProvider</code> -- abstract interface that all providers implement</li> <li><code>ProviderRegistry</code> -- global registry mapping provider names to classes</li> <li><code>ProviderManager</code> -- high-level router that picks the best provider for each task</li> <li><code>discover_available_models()</code> -- scans all configured providers for available models</li> </ul>"},{"location":"api/providers/#baseprovider-abc","title":"BaseProvider (ABC)","text":"<pre><code>from video_processor.providers.base import BaseProvider\n</code></pre> <p>Abstract base class that all provider implementations must subclass. Defines the four core capabilities: chat, vision, audio transcription, and model listing.</p> <p>Class attribute:</p> Attribute Type Description <code>provider_name</code> <code>str</code> Identifier for this provider (e.g., <code>\"openai\"</code>, <code>\"anthropic\"</code>)"},{"location":"api/providers/#chat","title":"chat()","text":"<pre><code>def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n model: Optional[str] = None,\n) -> str\n</code></pre> <p>Send a chat completion request.</p> <p>Parameters:</p> Parameter Type Default Description <code>messages</code> <code>list[dict]</code> required OpenAI-format message list (<code>role</code>, <code>content</code>) <code>max_tokens</code> <code>int</code> <code>4096</code> Maximum tokens in the response <code>temperature</code> <code>float</code> <code>0.7</code> Sampling temperature <code>model</code> <code>Optional[str]</code> <code>None</code> Override model ID <p>Returns: <code>str</code> -- the assistant's text response.</p>"},{"location":"api/providers/#analyze_image","title":"analyze_image()","text":"<pre><code>def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n model: Optional[str] = None,\n) -> str\n</code></pre> <p>Analyze an image with a text prompt using a vision-capable model.</p> <p>Parameters:</p> Parameter Type Default Description <code>image_bytes</code> <code>bytes</code> required Raw image data (JPEG, PNG, etc.) <code>prompt</code> <code>str</code> required Analysis instructions <code>max_tokens</code> <code>int</code> <code>4096</code> Maximum tokens in the response <code>model</code> <code>Optional[str]</code> <code>None</code> Override model ID <p>Returns: <code>str</code> -- the assistant's analysis text.</p>"},{"location":"api/providers/#transcribe_audio","title":"transcribe_audio()","text":"<pre><code>def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n model: Optional[str] = None,\n) -> dict\n</code></pre> <p>Transcribe an audio file.</p> <p>Parameters:</p> Parameter Type Default Description <code>audio_path</code> <code>str \\| Path</code> required Path to the audio file <code>language</code> <code>Optional[str]</code> <code>None</code> Language hint (ISO 639-1 code) <code>model</code> <code>Optional[str]</code> <code>None</code> Override model ID <p>Returns: <code>dict</code> -- transcription result with keys <code>text</code>, <code>segments</code>, <code>duration</code>, etc.</p>"},{"location":"api/providers/#list_models","title":"list_models()","text":"<pre><code>def list_models(self) -> list[ModelInfo]\n</code></pre> <p>Discover available models from this provider's API.</p> <p>Returns: <code>list[ModelInfo]</code> -- available models with capability metadata.</p>"},{"location":"api/providers/#modelinfo","title":"ModelInfo","text":"<pre><code>from video_processor.providers.base import ModelInfo\n</code></pre> <p>Pydantic model describing an available model from a provider.</p> Field Type Default Description <code>id</code> <code>str</code> required Model identifier (e.g., <code>\"gpt-4o\"</code>, <code>\"claude-haiku-4-5-20251001\"</code>) <code>provider</code> <code>str</code> required Provider name (e.g., <code>\"openai\"</code>, <code>\"anthropic\"</code>, <code>\"gemini\"</code>) <code>display_name</code> <code>str</code> <code>\"\"</code> Human-readable display name <code>capabilities</code> <code>List[str]</code> <code>[]</code> Model capabilities: <code>\"chat\"</code>, <code>\"vision\"</code>, <code>\"audio\"</code>, <code>\"embedding\"</code> <pre><code>{\n \"id\": \"gpt-4o\",\n \"provider\": \"openai\",\n \"display_name\": \"GPT-4o\",\n \"capabilities\": [\"chat\", \"vision\"]\n}\n</code></pre>"},{"location":"api/providers/#providerregistry","title":"ProviderRegistry","text":"<pre><code>from video_processor.providers.base import ProviderRegistry\n</code></pre> <p>Class-level registry for provider classes. Providers register themselves with metadata on import. This registry is used internally by <code>ProviderManager</code> but can also be used directly for introspection.</p>"},{"location":"api/providers/#register","title":"register()","text":"<pre><code>@classmethod\ndef register(\n cls,\n name: str,\n provider_class: type,\n env_var: str = \"\",\n model_prefixes: Optional[List[str]] = None,\n default_models: Optional[Dict[str, str]] = None,\n) -> None\n</code></pre> <p>Register a provider class with its metadata. Called by each provider module at import time.</p> <p>Parameters:</p> Parameter Type Default Description <code>name</code> <code>str</code> required Provider name (e.g., <code>\"openai\"</code>) <code>provider_class</code> <code>type</code> required The provider class <code>env_var</code> <code>str</code> <code>\"\"</code> Environment variable for API key <code>model_prefixes</code> <code>Optional[List[str]]</code> <code>None</code> Model ID prefixes for auto-detection (e.g., <code>[\"gpt-\", \"o1-\"]</code>) <code>default_models</code> <code>Optional[Dict[str, str]]</code> <code>None</code> Default models per capability (e.g., <code>{\"chat\": \"gpt-4o\", \"vision\": \"gpt-4o\"}</code>)"},{"location":"api/providers/#get","title":"get()","text":"<pre><code>@classmethod\ndef get(cls, name: str) -> type\n</code></pre> <p>Return the provider class for a given name. Raises <code>ValueError</code> if the provider is not registered.</p>"},{"location":"api/providers/#get_by_model","title":"get_by_model()","text":"<pre><code>@classmethod\ndef get_by_model(cls, model_id: str) -> Optional[str]\n</code></pre> <p>Return the provider name for a model ID based on prefix matching. Returns <code>None</code> if no match is found.</p>"},{"location":"api/providers/#get_default_models","title":"get_default_models()","text":"<pre><code>@classmethod\ndef get_default_models(cls, name: str) -> Dict[str, str]\n</code></pre> <p>Return the default models dict for a provider, mapping capability names to model IDs.</p>"},{"location":"api/providers/#available","title":"available()","text":"<pre><code>@classmethod\ndef available(cls) -> List[str]\n</code></pre> <p>Return names of providers whose required environment variable is set (or providers with no env var requirement, like Ollama).</p>"},{"location":"api/providers/#all_registered","title":"all_registered()","text":"<pre><code>@classmethod\ndef all_registered(cls) -> Dict[str, Dict]\n</code></pre> <p>Return all registered providers and their metadata dictionaries.</p>"},{"location":"api/providers/#openaicompatibleprovider","title":"OpenAICompatibleProvider","text":"<pre><code>from video_processor.providers.base import OpenAICompatibleProvider\n</code></pre> <p>Base class for providers using OpenAI-compatible APIs (Together, Fireworks, Cerebras, xAI, Azure). Implements <code>chat()</code>, <code>analyze_image()</code>, and <code>list_models()</code> using the OpenAI client library. <code>transcribe_audio()</code> raises <code>NotImplementedError</code> by default.</p> <p>Constructor:</p> <pre><code>def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None)\n</code></pre> Parameter Type Default Description <code>api_key</code> <code>Optional[str]</code> <code>None</code> API key (falls back to <code>self.env_var</code> environment variable) <code>base_url</code> <code>Optional[str]</code> <code>None</code> API base URL (falls back to <code>self.base_url</code> class attribute) <p>Subclass attributes to override:</p> Attribute Description <code>provider_name</code> Provider identifier string <code>base_url</code> Default API base URL <code>env_var</code> Environment variable name for the API key <p>Usage tracking: After each <code>chat()</code> or <code>analyze_image()</code> call, the provider stores token counts in <code>self._last_usage</code> as <code>{\"input_tokens\": int, \"output_tokens\": int}</code>. This is consumed by <code>ProviderManager._track()</code>.</p>"},{"location":"api/providers/#providermanager","title":"ProviderManager","text":"<pre><code>from video_processor.providers.manager import ProviderManager\n</code></pre> <p>High-level router that selects the best available provider and model for each API call. Supports explicit model selection, forced provider, or automatic selection based on discovered capabilities.</p>"},{"location":"api/providers/#constructor","title":"Constructor","text":"<pre><code>def __init__(\n self,\n vision_model: Optional[str] = None,\n chat_model: Optional[str] = None,\n transcription_model: Optional[str] = None,\n provider: Optional[str] = None,\n auto: bool = True,\n)\n</code></pre> Parameter Type Default Description <code>vision_model</code> <code>Optional[str]</code> <code>None</code> Override model for vision tasks (e.g., <code>\"gpt-4o\"</code>) <code>chat_model</code> <code>Optional[str]</code> <code>None</code> Override model for chat/LLM tasks <code>transcription_model</code> <code>Optional[str]</code> <code>None</code> Override model for transcription <code>provider</code> <code>Optional[str]</code> <code>None</code> Force all tasks to a single provider <code>auto</code> <code>bool</code> <code>True</code> If <code>True</code> and no model specified, pick the best available <p>Attributes:</p> Attribute Type Description <code>usage</code> <code>UsageTracker</code> Tracks token counts and API costs across all calls"},{"location":"api/providers/#auto-selection-preferences","title":"Auto-selection preferences","text":"<p>When <code>auto=True</code> and no explicit model is set, providers are tried in this order:</p> <p>Vision: Gemini (<code>gemini-2.5-flash</code>) > OpenAI (<code>gpt-4o-mini</code>) > Anthropic (<code>claude-haiku-4-5-20251001</code>)</p> <p>Chat: Anthropic (<code>claude-haiku-4-5-20251001</code>) > OpenAI (<code>gpt-4o-mini</code>) > Gemini (<code>gemini-2.5-flash</code>)</p> <p>Transcription: OpenAI (<code>whisper-1</code>) > Gemini (<code>gemini-2.5-flash</code>)</p> <p>If no API-key-based provider is available, Ollama is tried as a fallback.</p>"},{"location":"api/providers/#chat_1","title":"chat()","text":"<pre><code>def chat(\n self,\n messages: list[dict],\n max_tokens: int = 4096,\n temperature: float = 0.7,\n) -> str\n</code></pre> <p>Send a chat completion to the best available provider. Automatically resolves which provider and model to use.</p> <p>Parameters:</p> Parameter Type Default Description <code>messages</code> <code>list[dict]</code> required OpenAI-format messages <code>max_tokens</code> <code>int</code> <code>4096</code> Maximum response tokens <code>temperature</code> <code>float</code> <code>0.7</code> Sampling temperature <p>Returns: <code>str</code> -- assistant response text.</p> <p>Raises: <code>RuntimeError</code> if no provider is available for the <code>chat</code> capability.</p>"},{"location":"api/providers/#analyze_image_1","title":"analyze_image()","text":"<pre><code>def analyze_image(\n self,\n image_bytes: bytes,\n prompt: str,\n max_tokens: int = 4096,\n) -> str\n</code></pre> <p>Analyze an image using the best available vision provider.</p> <p>Returns: <code>str</code> -- analysis text.</p> <p>Raises: <code>RuntimeError</code> if no provider is available for the <code>vision</code> capability.</p>"},{"location":"api/providers/#transcribe_audio_1","title":"transcribe_audio()","text":"<pre><code>def transcribe_audio(\n self,\n audio_path: str | Path,\n language: Optional[str] = None,\n speaker_hints: Optional[list[str]] = None,\n) -> dict\n</code></pre> <p>Transcribe audio. Prefers local Whisper (no file size limits, no API costs) when available, falling back to API-based transcription.</p> <p>Parameters:</p> Parameter Type Default Description <code>audio_path</code> <code>str \\| Path</code> required Path to the audio file <code>language</code> <code>Optional[str]</code> <code>None</code> Language hint <code>speaker_hints</code> <code>Optional[list[str]]</code> <code>None</code> Speaker names for better recognition <p>Returns: <code>dict</code> -- transcription result with <code>text</code>, <code>segments</code>, <code>duration</code>.</p> <p>Local Whisper: If <code>transcription_model</code> is unset or starts with <code>\"whisper-local\"</code>, the manager tries local Whisper first. Use <code>\"whisper-local:large\"</code> to specify a model size.</p>"},{"location":"api/providers/#get_models_used","title":"get_models_used()","text":"<pre><code>def get_models_used(self) -> dict[str, str]\n</code></pre> <p>Return a dict mapping capability to <code>\"provider/model\"</code> string for tracking purposes.</p> <pre><code>pm = ProviderManager()\nprint(pm.get_models_used())\n# {\"vision\": \"gemini/gemini-2.5-flash\", \"chat\": \"anthropic/claude-haiku-4-5-20251001\", ...}\n</code></pre>"},{"location":"api/providers/#usage-examples","title":"Usage examples","text":"<pre><code>from video_processor.providers.manager import ProviderManager\n\n# Auto-select best providers\npm = ProviderManager()\n\n# Force everything through one provider\npm = ProviderManager(provider=\"openai\")\n\n# Explicit model selection\npm = ProviderManager(\n vision_model=\"gpt-4o\",\n chat_model=\"claude-haiku-4-5-20251001\",\n transcription_model=\"whisper-local:large\",\n)\n\n# Chat completion\nresponse = pm.chat([\n {\"role\": \"user\", \"content\": \"Summarize this meeting transcript...\"}\n])\n\n# Image analysis\nwith open(\"diagram.png\", \"rb\") as f:\n analysis = pm.analyze_image(f.read(), \"Describe this architecture diagram\")\n\n# Transcription with speaker hints\nresult = pm.transcribe_audio(\n \"meeting.mp3\",\n language=\"en\",\n speaker_hints=[\"Alice\", \"Bob\", \"Charlie\"],\n)\n\n# Check usage\nprint(pm.usage.summary())\n</code></pre>"},{"location":"api/providers/#discover_available_models","title":"discover_available_models()","text":"<pre><code>from video_processor.providers.discovery import discover_available_models\n</code></pre> <pre><code>def discover_available_models(\n api_keys: Optional[dict[str, str]] = None,\n force_refresh: bool = False,\n) -> list[ModelInfo]\n</code></pre> <p>Discover available models from all configured providers. For each provider with a valid API key, calls <code>list_models()</code> and returns a unified, sorted list.</p> <p>Parameters:</p> Parameter Type Default Description <code>api_keys</code> <code>Optional[dict[str, str]]</code> <code>None</code> Override API keys (defaults to environment variables) <code>force_refresh</code> <code>bool</code> <code>False</code> Force re-discovery, ignoring the session cache <p>Returns: <code>list[ModelInfo]</code> -- all discovered models, sorted by provider then model ID.</p> <p>Caching: Results are cached for the session. Use <code>force_refresh=True</code> or <code>clear_discovery_cache()</code> to refresh.</p> <pre><code>from video_processor.providers.discovery import (\n discover_available_models,\n clear_discovery_cache,\n)\n\n# Discover models using environment variables\nmodels = discover_available_models()\nfor m in models:\n print(f\"{m.provider}/{m.id} - {m.capabilities}\")\n\n# Force refresh\nmodels = discover_available_models(force_refresh=True)\n\n# Override API keys\nmodels = discover_available_models(api_keys={\n \"openai\": \"sk-...\",\n \"anthropic\": \"sk-ant-...\",\n})\n\n# Clear cache\nclear_discovery_cache()\n</code></pre>"},{"location":"api/providers/#clear_discovery_cache","title":"clear_discovery_cache()","text":"<pre><code>def clear_discovery_cache() -> None\n</code></pre> <p>Clear the cached model list, forcing the next <code>discover_available_models()</code> call to re-query providers.</p>"},{"location":"api/providers/#built-in-providers","title":"Built-in Providers","text":"<p>The following providers are registered automatically when the provider system initializes:</p> Provider Environment Variable Capabilities Default Chat Model <code>openai</code> <code>OPENAI_API_KEY</code> chat, vision, audio <code>gpt-4o-mini</code> <code>anthropic</code> <code>ANTHROPIC_API_KEY</code> chat, vision <code>claude-haiku-4-5-20251001</code> <code>gemini</code> <code>GEMINI_API_KEY</code> chat, vision, audio <code>gemini-2.5-flash</code> <code>ollama</code> (none -- checks server) chat, vision (depends on installed models) <code>together</code> <code>TOGETHER_API_KEY</code> chat (varies) <code>fireworks</code> <code>FIREWORKS_API_KEY</code> chat (varies) <code>cerebras</code> <code>CEREBRAS_API_KEY</code> chat (varies) <code>xai</code> <code>XAI_API_KEY</code> chat (varies) <code>azure</code> <code>AZURE_OPENAI_API_KEY</code> chat, vision (varies)"},{"location":"api/sources/","title":"Sources API Reference","text":""},{"location":"api/sources/#video_processor.sources.base","title":"<code>video_processor.sources.base</code>","text":"<p>Base interface for cloud source integrations.</p>"},{"location":"api/sources/#video_processor.sources.base.BaseSource","title":"<code>BaseSource</code>","text":"<p> Bases: <code>ABC</code></p> <p>Abstract base class for cloud source integrations.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>class BaseSource(ABC):\n \"\"\"Abstract base class for cloud source integrations.\"\"\"\n\n @abstractmethod\n def authenticate(self) -> bool:\n \"\"\"Authenticate with the cloud provider. Returns True on success.\"\"\"\n ...\n\n @abstractmethod\n def list_videos(\n self,\n folder_id: Optional[str] = None,\n folder_path: Optional[str] = None,\n patterns: Optional[List[str]] = None,\n ) -> List[SourceFile]:\n \"\"\"List video files in a folder.\"\"\"\n ...\n\n @abstractmethod\n def download(\n self,\n file: SourceFile,\n destination: Path,\n ) -> Path:\n \"\"\"Download a file to a local path. Returns the local path.\"\"\"\n ...\n\n def download_all(\n self,\n files: List[SourceFile],\n destination_dir: Path,\n ) -> List[Path]:\n \"\"\"Download multiple files to a directory, preserving subfolder structure.\"\"\"\n destination_dir.mkdir(parents=True, exist_ok=True)\n paths = []\n for f in files:\n # Use path (with subfolder) if available, otherwise just name\n relative = f.path if f.path else f.name\n dest = destination_dir / relative\n try:\n local_path = self.download(f, dest)\n paths.append(local_path)\n logger.info(f\"Downloaded: {relative}\")\n except Exception as e:\n logger.error(f\"Failed to download {relative}: {e}\")\n return paths\n</code></pre>"},{"location":"api/sources/#video_processor.sources.base.BaseSource.authenticate","title":"<code>authenticate()</code> <code>abstractmethod</code>","text":"<p>Authenticate with the cloud provider. Returns True on success.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>@abstractmethod\ndef authenticate(self) -> bool:\n \"\"\"Authenticate with the cloud provider. Returns True on success.\"\"\"\n ...\n</code></pre>"},{"location":"api/sources/#video_processor.sources.base.BaseSource.download","title":"<code>download(file, destination)</code> <code>abstractmethod</code>","text":"<p>Download a file to a local path. Returns the local path.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>@abstractmethod\ndef download(\n self,\n file: SourceFile,\n destination: Path,\n) -> Path:\n \"\"\"Download a file to a local path. Returns the local path.\"\"\"\n ...\n</code></pre>"},{"location":"api/sources/#video_processor.sources.base.BaseSource.download_all","title":"<code>download_all(files, destination_dir)</code>","text":"<p>Download multiple files to a directory, preserving subfolder structure.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>def download_all(\n self,\n files: List[SourceFile],\n destination_dir: Path,\n) -> List[Path]:\n \"\"\"Download multiple files to a directory, preserving subfolder structure.\"\"\"\n destination_dir.mkdir(parents=True, exist_ok=True)\n paths = []\n for f in files:\n # Use path (with subfolder) if available, otherwise just name\n relative = f.path if f.path else f.name\n dest = destination_dir / relative\n try:\n local_path = self.download(f, dest)\n paths.append(local_path)\n logger.info(f\"Downloaded: {relative}\")\n except Exception as e:\n logger.error(f\"Failed to download {relative}: {e}\")\n return paths\n</code></pre>"},{"location":"api/sources/#video_processor.sources.base.BaseSource.list_videos","title":"<code>list_videos(folder_id=None, folder_path=None, patterns=None)</code> <code>abstractmethod</code>","text":"<p>List video files in a folder.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>@abstractmethod\ndef list_videos(\n self,\n folder_id: Optional[str] = None,\n folder_path: Optional[str] = None,\n patterns: Optional[List[str]] = None,\n) -> List[SourceFile]:\n \"\"\"List video files in a folder.\"\"\"\n ...\n</code></pre>"},{"location":"api/sources/#video_processor.sources.base.SourceFile","title":"<code>SourceFile</code>","text":"<p> Bases: <code>BaseModel</code></p> <p>A file available in a cloud source.</p> Source code in <code>video_processor/sources/base.py</code> <pre><code>class SourceFile(BaseModel):\n \"\"\"A file available in a cloud source.\"\"\"\n\n name: str = Field(description=\"File name\")\n id: str = Field(description=\"Provider-specific file identifier\")\n size_bytes: Optional[int] = Field(default=None, description=\"File size in bytes\")\n mime_type: Optional[str] = Field(default=None, description=\"MIME type\")\n modified_at: Optional[str] = Field(default=None, description=\"Last modified timestamp\")\n path: Optional[str] = Field(default=None, description=\"Path within the source folder\")\n</code></pre>"},{"location":"api/sources/#overview","title":"Overview","text":"<p>The sources module provides a unified interface for fetching content from cloud services, local applications, and the web. All sources implement the <code>BaseSource</code> abstract class, providing consistent <code>authenticate()</code>, <code>list_videos()</code>, and <code>download()</code> methods.</p> <p>Sources are lazy-loaded to avoid pulling in optional dependencies at import time. You can import any source directly from <code>video_processor.sources</code> and the correct module will be loaded on demand.</p>"},{"location":"api/sources/#basesource-abc","title":"BaseSource (ABC)","text":"<pre><code>from video_processor.sources import BaseSource\n</code></pre> <p>Abstract base class that all source integrations implement. Defines the standard three-step workflow: authenticate, list, download.</p>"},{"location":"api/sources/#authenticate","title":"authenticate()","text":"<pre><code>@abstractmethod\ndef authenticate(self) -> bool\n</code></pre> <p>Authenticate with the cloud provider or service. Uses the auth strategy defined for the source (OAuth, API key, local access, etc.).</p> <p>Returns: <code>bool</code> -- <code>True</code> on successful authentication, <code>False</code> on failure.</p>"},{"location":"api/sources/#list_videos","title":"list_videos()","text":"<pre><code>@abstractmethod\ndef list_videos(\n self,\n folder_id: Optional[str] = None,\n folder_path: Optional[str] = None,\n patterns: Optional[List[str]] = None,\n) -> List[SourceFile]\n</code></pre> <p>List available video files (or other content, depending on the source).</p> <p>Parameters:</p> Parameter Type Default Description <code>folder_id</code> <code>Optional[str]</code> <code>None</code> Provider-specific folder/container identifier <code>folder_path</code> <code>Optional[str]</code> <code>None</code> Path within the source (e.g., folder name) <code>patterns</code> <code>Optional[List[str]]</code> <code>None</code> File name glob patterns to filter results <p>Returns: <code>List[SourceFile]</code> -- available files matching the criteria.</p>"},{"location":"api/sources/#download","title":"download()","text":"<pre><code>@abstractmethod\ndef download(\n self,\n file: SourceFile,\n destination: Path,\n) -> Path\n</code></pre> <p>Download a single file to a local path.</p> <p>Parameters:</p> Parameter Type Description <code>file</code> <code>SourceFile</code> File descriptor from <code>list_videos()</code> <code>destination</code> <code>Path</code> Local destination path <p>Returns: <code>Path</code> -- the local path where the file was saved.</p>"},{"location":"api/sources/#download_all","title":"download_all()","text":"<pre><code>def download_all(\n self,\n files: List[SourceFile],\n destination_dir: Path,\n) -> List[Path]\n</code></pre> <p>Download multiple files to a directory, preserving subfolder structure from <code>SourceFile.path</code>. This is a concrete method provided by the base class.</p> <p>Parameters:</p> Parameter Type Description <code>files</code> <code>List[SourceFile]</code> Files to download <code>destination_dir</code> <code>Path</code> Base directory for downloads (created if needed) <p>Returns: <code>List[Path]</code> -- local paths of successfully downloaded files. Failed downloads are logged and skipped.</p>"},{"location":"api/sources/#sourcefile","title":"SourceFile","text":"<pre><code>from video_processor.sources import SourceFile\n</code></pre> <p>Pydantic model describing a file available in a cloud source.</p> Field Type Default Description <code>name</code> <code>str</code> required File name <code>id</code> <code>str</code> required Provider-specific file identifier <code>size_bytes</code> <code>Optional[int]</code> <code>None</code> File size in bytes <code>mime_type</code> <code>Optional[str]</code> <code>None</code> MIME type (e.g., <code>\"video/mp4\"</code>) <code>modified_at</code> <code>Optional[str]</code> <code>None</code> Last modified timestamp <code>path</code> <code>Optional[str]</code> <code>None</code> Path within the source folder (used for subfolder structure in <code>download_all</code>) <pre><code>{\n \"name\": \"sprint-review-2026-03-01.mp4\",\n \"id\": \"abc123def456\",\n \"size_bytes\": 524288000,\n \"mime_type\": \"video/mp4\",\n \"modified_at\": \"2026-03-01T14:30:00Z\",\n \"path\": \"recordings/march/sprint-review-2026-03-01.mp4\"\n}\n</code></pre>"},{"location":"api/sources/#lazy-loading-pattern","title":"Lazy Loading Pattern","text":"<p>All sources are lazy-loaded via <code>__getattr__</code> in the package <code>__init__.py</code>. This means importing <code>video_processor.sources</code> does not pull in any external dependencies (e.g., <code>google-auth</code>, <code>msal</code>, <code>notion-client</code>). The actual module is loaded only when you access the class.</p> <pre><code># This import is instant -- no dependencies loaded\nfrom video_processor.sources import ZoomSource\n\n# The zoom_source module (and its dependencies) are loaded here\nsource = ZoomSource()\n</code></pre>"},{"location":"api/sources/#available-sources","title":"Available Sources","text":""},{"location":"api/sources/#cloud-recordings","title":"Cloud Recordings","text":"<p>Sources for fetching recorded meetings from video conferencing platforms.</p> Source Class Auth Method Description Zoom <code>ZoomSource</code> OAuth / Server-to-Server List and download Zoom cloud recordings Google Meet <code>MeetRecordingSource</code> OAuth (Google) List and download Google Meet recordings from Drive Microsoft Teams <code>TeamsRecordingSource</code> OAuth (Microsoft) List and download Teams meeting recordings"},{"location":"api/sources/#cloud-storage-and-workspace","title":"Cloud Storage and Workspace","text":"<p>Sources for accessing files stored in cloud platforms.</p> Source Class Auth Method Description Google Drive <code>GoogleDriveSource</code> OAuth (Google) Files from Google Drive Google Workspace <code>GWSSource</code> OAuth (Google) Google Docs, Sheets, Slides Microsoft 365 <code>M365Source</code> OAuth (Microsoft) OneDrive, SharePoint files Notion <code>NotionSource</code> OAuth / API key Notion pages and databases GitHub <code>GitHubSource</code> OAuth / API token Repository files, issues, discussions Dropbox <code>DropboxSource</code> OAuth / access token (via auth config)"},{"location":"api/sources/#notes-applications","title":"Notes Applications","text":"<p>Sources for local and cloud-based note-taking apps.</p> Source Class Auth Method Description Apple Notes <code>AppleNotesSource</code> Local (macOS) Notes from Apple Notes.app Obsidian <code>ObsidianSource</code> Local filesystem Markdown files from Obsidian vaults Logseq <code>LogseqSource</code> Local filesystem Pages from Logseq graphs OneNote <code>OneNoteSource</code> OAuth (Microsoft) Microsoft OneNote notebooks Google Keep <code>GoogleKeepSource</code> OAuth (Google) Google Keep notes"},{"location":"api/sources/#web-and-content","title":"Web and Content","text":"<p>Sources for fetching content from the web.</p> Source Class Auth Method Description YouTube <code>YouTubeSource</code> API key / OAuth YouTube video metadata and transcripts Web <code>WebSource</code> None General web page content extraction RSS <code>RSSSource</code> None RSS/Atom feed entries Podcast <code>PodcastSource</code> None Podcast episodes from RSS feeds arXiv <code>ArxivSource</code> None Academic papers from arXiv Hacker News <code>HackerNewsSource</code> None Hacker News posts and comments Reddit <code>RedditSource</code> API credentials Reddit posts and comments Twitter/X <code>TwitterSource</code> API credentials Tweets and threads"},{"location":"api/sources/#auth-integration","title":"Auth Integration","text":"<p>Most sources use PlanOpticon's unified auth system (see Auth API). The typical pattern within a source implementation:</p> <pre><code>from video_processor.auth import get_auth_manager\n\nclass MySource(BaseSource):\n def __init__(self):\n self._token = None\n\n def authenticate(self) -> bool:\n manager = get_auth_manager(\"my_service\")\n if manager:\n token = manager.get_token()\n if token:\n self._token = token\n return True\n return False\n\n def list_videos(self, **kwargs) -> list[SourceFile]:\n if not self._token:\n raise RuntimeError(\"Not authenticated. Call authenticate() first.\")\n # Use self._token to call the API\n ...\n</code></pre>"},{"location":"api/sources/#usage-examples","title":"Usage Examples","text":""},{"location":"api/sources/#listing-and-downloading-zoom-recordings","title":"Listing and downloading Zoom recordings","text":"<pre><code>from pathlib import Path\nfrom video_processor.sources import ZoomSource\n\nsource = ZoomSource()\nif source.authenticate():\n recordings = source.list_videos()\n for rec in recordings:\n print(f\"{rec.name} ({rec.size_bytes} bytes)\")\n\n # Download all to a local directory\n paths = source.download_all(recordings, Path(\"./downloads\"))\n</code></pre>"},{"location":"api/sources/#fetching-from-multiple-sources","title":"Fetching from multiple sources","text":"<pre><code>from pathlib import Path\nfrom video_processor.sources import GoogleDriveSource, NotionSource\n\n# Google Drive\ngdrive = GoogleDriveSource()\nif gdrive.authenticate():\n files = gdrive.list_videos(\n folder_path=\"Meeting Recordings\",\n patterns=[\"*.mp4\", \"*.webm\"],\n )\n gdrive.download_all(files, Path(\"./drive-downloads\"))\n\n# Notion\nnotion = NotionSource()\nif notion.authenticate():\n pages = notion.list_videos() # Lists Notion pages\n for page in pages:\n print(f\"Page: {page.name}\")\n</code></pre>"},{"location":"api/sources/#youtube-content","title":"YouTube content","text":"<pre><code>from video_processor.sources import YouTubeSource\n\nyt = YouTubeSource()\nif yt.authenticate():\n videos = yt.list_videos(folder_path=\"https://youtube.com/playlist?list=...\")\n for v in videos:\n print(f\"{v.name} - {v.id}\")\n</code></pre>"},{"location":"architecture/overview/","title":"Architecture Overview","text":""},{"location":"architecture/overview/#system-diagram","title":"System diagram","text":"<pre><code>graph TD\n subgraph Sources\n S1[Video Files]\n S2[Google Workspace]\n S3[Microsoft 365]\n S4[Zoom / Teams / Meet]\n S5[YouTube]\n S6[Notes \u2014 Obsidian / Notion / Apple Notes]\n S7[GitHub]\n end\n\n subgraph Source Connectors\n SC[Source Connectors + OAuth]\n end\n\n S1 --> SC\n S2 --> SC\n S3 --> SC\n S4 --> SC\n S5 --> SC\n S6 --> SC\n S7 --> SC\n\n SC --> A[Ingest / Analyze Pipeline]\n\n A --> B[Frame Extractor]\n A --> C[Audio Extractor]\n B --> D[Diagram Analyzer]\n C --> E[Transcription]\n D --> F[Knowledge Graph]\n E --> F\n E --> G[Key Point Extractor]\n E --> H[Action Item Detector]\n D --> I[Content Analyzer]\n E --> I\n\n subgraph Agent & Skills\n AG[Planning Agent]\n SK[Skill Registry]\n CO[Companion REPL]\n end\n\n F --> AG\n G --> AG\n H --> AG\n I --> AG\n AG --> SK\n F --> CO\n\n F --> J[Plan Generator]\n G --> J\n H --> J\n I --> J\n\n subgraph Output & Export\n J --> K[Markdown Report]\n J --> L[HTML Report]\n J --> M[PDF Report]\n D --> N[Mermaid/SVG/PNG Export]\n EX[Exporters \u2014 Obsidian / Notion / Exchange / Wiki]\n end\n\n AG --> EX\n F --> EX</code></pre>"},{"location":"architecture/overview/#module-structure","title":"Module structure","text":"<pre><code>video_processor/\n\u251c\u2500\u2500 cli/ # CLI commands (Click)\n\u2502 \u2514\u2500\u2500 commands.py\n\u251c\u2500\u2500 sources/ # Source connectors\n\u2502 \u251c\u2500\u2500 gdrive.py # Google Drive\n\u2502 \u251c\u2500\u2500 gws.py # Google Workspace (Docs, Sheets, Slides, Meet)\n\u2502 \u251c\u2500\u2500 m365.py # Microsoft 365 (OneDrive, SharePoint, Teams)\n\u2502 \u251c\u2500\u2500 dropbox.py # Dropbox\n\u2502 \u251c\u2500\u2500 zoom.py # Zoom recordings\n\u2502 \u251c\u2500\u2500 youtube.py # YouTube videos\n\u2502 \u251c\u2500\u2500 notion.py # Notion pages\n\u2502 \u251c\u2500\u2500 github.py # GitHub repos / wikis\n\u2502 \u251c\u2500\u2500 obsidian.py # Obsidian vaults\n\u2502 \u2514\u2500\u2500 apple_notes.py # Apple Notes (macOS)\n\u251c\u2500\u2500 extractors/ # Media extraction\n\u2502 \u251c\u2500\u2500 frame_extractor.py # Video \u2192 frames\n\u2502 \u2514\u2500\u2500 audio_extractor.py # Video \u2192 WAV\n\u251c\u2500\u2500 analyzers/ # AI-powered analysis\n\u2502 \u251c\u2500\u2500 diagram_analyzer.py # Frame classification + extraction\n\u2502 \u251c\u2500\u2500 content_analyzer.py # Cross-referencing\n\u2502 \u2514\u2500\u2500 action_detector.py # Action item detection\n\u251c\u2500\u2500 integrators/ # Knowledge assembly\n\u2502 \u251c\u2500\u2500 knowledge_graph.py # Entity/relationship graph\n\u2502 \u251c\u2500\u2500 graph_query.py # Query engine\n\u2502 \u2514\u2500\u2500 plan_generator.py # Report generation\n\u251c\u2500\u2500 agent/ # Planning agent\n\u2502 \u251c\u2500\u2500 agent.py # Agent loop\n\u2502 \u251c\u2500\u2500 skills.py # Skill registry\n\u2502 \u2514\u2500\u2500 companion.py # Companion REPL\n\u251c\u2500\u2500 exporters/ # Export formats\n\u2502 \u251c\u2500\u2500 markdown.py # Markdown export\n\u2502 \u251c\u2500\u2500 obsidian.py # Obsidian vault export\n\u2502 \u251c\u2500\u2500 notion.py # Notion export\n\u2502 \u251c\u2500\u2500 wiki.py # Wiki generation + push\n\u2502 \u2514\u2500\u2500 exchange.py # PlanOpticon Exchange Format\n\u251c\u2500\u2500 providers/ # AI provider abstraction\n\u2502 \u251c\u2500\u2500 base.py # BaseProvider ABC\n\u2502 \u251c\u2500\u2500 openai_provider.py\n\u2502 \u251c\u2500\u2500 anthropic_provider.py\n\u2502 \u251c\u2500\u2500 gemini_provider.py\n\u2502 \u251c\u2500\u2500 ollama_provider.py # Local Ollama (offline)\n\u2502 \u251c\u2500\u2500 azure_provider.py # Azure OpenAI\n\u2502 \u251c\u2500\u2500 together_provider.py\n\u2502 \u251c\u2500\u2500 fireworks_provider.py\n\u2502 \u251c\u2500\u2500 cerebras_provider.py\n\u2502 \u251c\u2500\u2500 xai_provider.py # xAI / Grok\n\u2502 \u251c\u2500\u2500 discovery.py # Auto-model-discovery\n\u2502 \u2514\u2500\u2500 manager.py # ProviderManager routing\n\u251c\u2500\u2500 utils/\n\u2502 \u251c\u2500\u2500 json_parsing.py # Robust LLM JSON parsing\n\u2502 \u251c\u2500\u2500 rendering.py # Mermaid + chart rendering\n\u2502 \u251c\u2500\u2500 export.py # HTML/PDF export\n\u2502 \u251c\u2500\u2500 api_cache.py # Disk-based response cache\n\u2502 \u2514\u2500\u2500 prompt_templates.py # LLM prompt management\n\u251c\u2500\u2500 auth.py # OAuth flow management\n\u251c\u2500\u2500 exchange.py # Exchange format schema\n\u251c\u2500\u2500 models.py # Pydantic data models\n\u251c\u2500\u2500 output_structure.py # Directory layout + manifest I/O\n\u2514\u2500\u2500 pipeline.py # Core processing pipeline\n</code></pre>"},{"location":"architecture/overview/#key-design-decisions","title":"Key design decisions","text":"<ul> <li>Pydantic everywhere \u2014 All structured data uses pydantic models for validation and serialization</li> <li>Manifest-driven \u2014 Every run produces <code>manifest.json</code> as the single source of truth</li> <li>Provider abstraction \u2014 Single <code>ProviderManager</code> wraps OpenAI, Anthropic, Gemini, Ollama, and additional providers behind a common interface</li> <li>No hardcoded models \u2014 Model lists come from API discovery</li> <li>Screengrab fallback \u2014 When extraction fails, save the frame as a captioned screenshot</li> <li>OAuth-first auth \u2014 All cloud service integrations use OAuth via <code>planopticon auth</code>, with credentials stored locally. Service account keys are supported as a fallback for server-side automation</li> <li>Skill registry \u2014 The planning agent discovers and invokes skills dynamically. Skills are self-describing and can be composed by the agent to accomplish complex tasks</li> <li>Exchange format \u2014 A portable JSON format (<code>exchange.py</code>) for importing and exporting knowledge graphs between PlanOpticon instances and external tools</li> </ul>"},{"location":"architecture/pipeline/","title":"Processing Pipeline","text":"<p>PlanOpticon has four main pipelines: video analysis, document ingestion, source connector, and export. Each pipeline can operate independently, and they connect through the shared knowledge graph.</p>"},{"location":"architecture/pipeline/#single-video-pipeline","title":"Single video pipeline","text":"<p>The core video analysis pipeline processes a single video file through eight sequential steps with checkpoint/resume support.</p> <pre><code>sequenceDiagram\n participant CLI\n participant Pipeline\n participant FrameExtractor\n participant AudioExtractor\n participant Provider\n participant DiagramAnalyzer\n participant KnowledgeGraph\n participant Exporter\n\n CLI->>Pipeline: process_single_video()\n\n Note over Pipeline: Step 1: Extract frames\n Pipeline->>FrameExtractor: extract_frames()\n Note over FrameExtractor: Change detection + periodic capture (every 30s)\n FrameExtractor-->>Pipeline: frame_paths[]\n\n Note over Pipeline: Step 2: Filter people frames\n Pipeline->>Pipeline: filter_people_frames()\n Note over Pipeline: OpenCV face detection removes webcam/people frames\n\n Note over Pipeline: Step 3: Extract + transcribe audio\n Pipeline->>AudioExtractor: extract_audio()\n Pipeline->>Provider: transcribe_audio()\n Note over Provider: Supports speaker hints via --speakers flag\n\n Note over Pipeline: Step 4: Analyze visuals\n Pipeline->>DiagramAnalyzer: process_frames()\n loop Each frame (up to 10 standard / 20 comprehensive)\n DiagramAnalyzer->>Provider: classify (vision)\n alt High confidence diagram\n DiagramAnalyzer->>Provider: full analysis\n Note over Provider: Extract description, text, mermaid, chart data\n else Medium confidence\n DiagramAnalyzer-->>Pipeline: screengrab fallback\n end\n end\n\n Note over Pipeline: Step 5: Build knowledge graph\n Pipeline->>KnowledgeGraph: register_source()\n Pipeline->>KnowledgeGraph: process_transcript()\n Pipeline->>KnowledgeGraph: process_diagrams()\n Note over KnowledgeGraph: Writes knowledge_graph.db (SQLite) + .json\n\n Note over Pipeline: Step 6: Extract key points + action items\n Pipeline->>Provider: extract key points\n Pipeline->>Provider: extract action items\n\n Note over Pipeline: Step 7: Generate report\n Pipeline->>Pipeline: generate markdown report\n Note over Pipeline: Includes mermaid diagrams, tables, cross-references\n\n Note over Pipeline: Step 8: Export formats\n Pipeline->>Exporter: export_all_formats()\n Note over Exporter: HTML report, PDF, SVG/PNG renderings, chart reproductions\n\n Pipeline-->>CLI: VideoManifest</code></pre>"},{"location":"architecture/pipeline/#pipeline-steps-in-detail","title":"Pipeline steps in detail","text":"Step Name Checkpointable Description 1 Extract frames Yes Change detection + periodic capture. Skipped if <code>frames/frame_*.jpg</code> exist on disk. 2 Filter people frames No Inline with step 1. OpenCV face detection removes webcam frames. 3 Extract + transcribe audio Yes Skipped if <code>transcript/transcript.json</code> exists. Speaker hints passed if <code>--speakers</code> provided. 4 Analyze visuals Yes Skipped if <code>diagrams/</code> is populated. Evenly samples frames (not just first N). 5 Build knowledge graph Yes Skipped if <code>results/knowledge_graph.db</code> exists. Registers source, processes transcript and diagrams. 6 Extract key points + actions Yes Skipped if <code>results/key_points.json</code> and <code>results/action_items.json</code> exist. 7 Generate report Yes Skipped if <code>results/analysis.md</code> exists. 8 Export formats No Always runs. Renders mermaid to SVG/PNG, reproduces charts, generates HTML/PDF."},{"location":"architecture/pipeline/#batch-pipeline","title":"Batch pipeline","text":"<p>The batch pipeline wraps the single-video pipeline and adds cross-video knowledge graph merging.</p> <pre><code>flowchart TD\n A[Scan input directory] --> B[Match video files by pattern]\n B --> C{For each video}\n C --> D[process_single_video]\n D --> E{Success?}\n E -->|Yes| F[Collect manifest + KG]\n E -->|No| G[Log error, continue]\n F --> H[Next video]\n G --> H\n H --> C\n C -->|All done| I[Merge knowledge graphs]\n I --> J[Fuzzy matching + conflict resolution]\n J --> K[Generate batch summary]\n K --> L[Write batch manifest]\n L --> M[batch_manifest.json + batch_summary.md + merged KG]</code></pre>"},{"location":"architecture/pipeline/#knowledge-graph-merge-strategy","title":"Knowledge graph merge strategy","text":"<p>During batch merging, <code>KnowledgeGraph.merge()</code> applies:</p> <ol> <li>Case-insensitive exact matching for entity names</li> <li>Fuzzy matching via <code>SequenceMatcher</code> (threshold >= 0.85) for near-duplicates</li> <li>Type conflict resolution using a specificity ranking (e.g., <code>technology</code> > <code>concept</code>)</li> <li>Description union across all sources</li> <li>Relationship deduplication by (source, target, type) tuple</li> </ol>"},{"location":"architecture/pipeline/#document-ingestion-pipeline","title":"Document ingestion pipeline","text":"<p>The document ingestion pipeline processes files (Markdown, plaintext, PDF) into knowledge graphs without video analysis.</p> <pre><code>flowchart TD\n A[Input: file or directory] --> B{File or directory?}\n B -->|File| C[get_processor by extension]\n B -->|Directory| D[Glob for supported extensions]\n D --> E{Recursive?}\n E -->|Yes| F[rglob all files]\n E -->|No| G[glob top-level only]\n F --> H[For each file]\n G --> H\n H --> C\n C --> I[DocumentProcessor.process]\n I --> J[DocumentChunk list]\n J --> K[Register source in KG]\n K --> L[Add chunks as content]\n L --> M[KG extracts entities + relationships]\n M --> N[knowledge_graph.db]</code></pre>"},{"location":"architecture/pipeline/#supported-document-types","title":"Supported document types","text":"Extension Processor Notes <code>.md</code> <code>MarkdownProcessor</code> Splits by headings into sections <code>.txt</code> <code>PlaintextProcessor</code> Splits into fixed-size chunks <code>.pdf</code> <code>PdfProcessor</code> Requires <code>pymupdf</code> or <code>pdfplumber</code>. Falls back gracefully between libraries."},{"location":"architecture/pipeline/#adding-documents-to-an-existing-graph","title":"Adding documents to an existing graph","text":"<p>The <code>--db-path</code> flag lets you ingest documents into an existing knowledge graph:</p> <pre><code>planopticon ingest spec.md --db-path existing.db\nplanopticon ingest ./docs/ -o ./output --recursive\n</code></pre>"},{"location":"architecture/pipeline/#source-connector-pipeline","title":"Source connector pipeline","text":"<p>Source connectors fetch content from cloud services, note-taking apps, and web sources. Each source implements the <code>BaseSource</code> ABC with three methods: <code>authenticate()</code>, <code>list_videos()</code>, and <code>download()</code>.</p> <pre><code>flowchart TD\n A[Source command] --> B[Authenticate with provider]\n B --> C{Auth success?}\n C -->|No| D[Error: check credentials]\n C -->|Yes| E[List files in folder]\n E --> F[Filter by pattern / type]\n F --> G[Download to local path]\n G --> H{Analyze or ingest?}\n H -->|Video| I[process_single_video / batch]\n H -->|Document| J[ingest_file / ingest_directory]\n I --> K[Knowledge graph]\n J --> K</code></pre>"},{"location":"architecture/pipeline/#available-sources","title":"Available sources","text":"<p>PlanOpticon includes connectors for:</p> Category Sources Cloud storage Google Drive, S3, Dropbox Meeting recordings Zoom, Google Meet, Microsoft Teams Productivity suites Google Workspace (Docs/Sheets/Slides), Microsoft 365 (SharePoint/OneDrive/OneNote) Note-taking apps Obsidian, Logseq, Apple Notes, Google Keep, Notion Web sources YouTube, Web (URL), RSS, Podcasts Developer platforms GitHub, arXiv Social media Reddit, Twitter/X, Hacker News <p>Each source authenticates via environment variables (API keys, OAuth tokens) specific to the provider.</p>"},{"location":"architecture/pipeline/#planning-agent-pipeline","title":"Planning agent pipeline","text":"<p>The planning agent consumes a knowledge graph and uses registered skills to generate planning artifacts.</p> <pre><code>flowchart TD\n A[Knowledge graph] --> B[Load into AgentContext]\n B --> C[GraphQueryEngine]\n C --> D[Taxonomy classification]\n D --> E[Agent orchestrator]\n E --> F{Select skill}\n F --> G[ProjectPlan skill]\n F --> H[PRD skill]\n F --> I[Roadmap skill]\n F --> J[TaskBreakdown skill]\n F --> K[DocGenerator skill]\n F --> L[WikiGenerator skill]\n F --> M[NotesExport skill]\n F --> N[ArtifactExport skill]\n F --> O[GitHubIntegration skill]\n F --> P[RequirementsChat skill]\n G --> Q[Artifact output]\n H --> Q\n I --> Q\n J --> Q\n K --> Q\n L --> Q\n M --> Q\n N --> Q\n O --> Q\n P --> Q\n Q --> R[Write to disk / push to service]</code></pre>"},{"location":"architecture/pipeline/#skill-execution-flow","title":"Skill execution flow","text":"<ol> <li>The <code>AgentContext</code> is populated with the knowledge graph, query engine, provider manager, and any planning entities from taxonomy classification</li> <li>Each <code>Skill</code> checks <code>can_execute()</code> against the context (requires at minimum a knowledge graph and provider manager)</li> <li>The skill's <code>execute()</code> method generates an <code>Artifact</code> with a name, content, type, and format</li> <li>Artifacts are collected and can be exported to disk or pushed to external services (GitHub issues, wiki pages, etc.)</li> </ol>"},{"location":"architecture/pipeline/#export-pipeline","title":"Export pipeline","text":"<p>The export pipeline converts knowledge graphs and analysis artifacts into various output formats.</p> <pre><code>flowchart TD\n A[knowledge_graph.db] --> B{Export command}\n B --> C[export markdown]\n B --> D[export obsidian]\n B --> E[export notion]\n B --> F[export exchange]\n B --> G[wiki generate]\n B --> H[kg convert]\n C --> I[7 document types + entity briefs + CSV]\n D --> J[Obsidian vault with frontmatter + wiki-links]\n E --> K[Notion-compatible markdown + CSV database]\n F --> L[PlanOpticonExchange JSON payload]\n G --> M[GitHub wiki pages + sidebar + home]\n H --> N[Convert between .db / .json / .graphml / .csv]</code></pre> <p>All export commands accept a <code>knowledge_graph.db</code> (or <code>.json</code>) path as input. No API key is required for template-based exports (markdown, obsidian, notion, wiki, exchange, convert). Only the planning agent skills that generate new content require a provider.</p>"},{"location":"architecture/pipeline/#how-pipelines-connect","title":"How pipelines connect","text":"<pre><code>flowchart LR\n V[Video files] --> VP[Video Pipeline]\n D[Documents] --> DI[Document Ingestion]\n S[Cloud Sources] --> SC[Source Connectors]\n SC --> V\n SC --> D\n VP --> KG[(knowledge_graph.db)]\n DI --> KG\n KG --> QE[Query Engine]\n KG --> EP[Export Pipeline]\n KG --> PA[Planning Agent]\n PA --> AR[Artifacts]\n AR --> EP</code></pre> <p>All pipelines converge on the knowledge graph as the central data store. The knowledge graph is the shared interface between ingestion (video or document), querying, exporting, and planning.</p>"},{"location":"architecture/pipeline/#error-handling","title":"Error handling","text":"<p>Error handling follows consistent patterns across all pipelines:</p> Scenario Behavior Video fails in batch Batch continues. Failed video recorded in manifest with error details. Diagram analysis fails Falls back to screengrab (captioned screenshot). LLM extraction fails Returns empty results gracefully. Key points and action items will be empty arrays. Document processor not found Raises <code>ValueError</code> with list of supported extensions. Source authentication fails Returns <code>False</code> from <code>authenticate()</code>. CLI prints error message. Checkpoint file found Step is skipped entirely and results are loaded from disk. Progress callback fails Warning logged. Pipeline continues without progress updates."},{"location":"architecture/pipeline/#progress-callback-system","title":"Progress callback system","text":"<p>The pipeline supports a <code>ProgressCallback</code> protocol for real-time progress tracking. This is used by the CLI's progress bars and can be implemented by external integrations (web UIs, CI systems, etc.).</p> <pre><code>from video_processor.models import ProgressCallback\n\nclass MyCallback:\n def on_step_start(self, step: str, index: int, total: int) -> None:\n print(f\"Starting step {index}/{total}: {step}\")\n\n def on_step_complete(self, step: str, index: int, total: int) -> None:\n print(f\"Completed step {index}/{total}: {step}\")\n\n def on_progress(self, step: str, percent: float, message: str = \"\") -> None:\n print(f\" {step}: {percent:.0%} {message}\")\n</code></pre> <p>Pass the callback to <code>process_single_video()</code>:</p> <pre><code>from video_processor.pipeline import process_single_video\n\nmanifest = process_single_video(\n input_path=\"recording.mp4\",\n output_dir=\"./output\",\n progress_callback=MyCallback(),\n)\n</code></pre> <p>The callback methods are called within a try/except wrapper, so a failing callback never interrupts the pipeline. If a callback method raises an exception, a warning is logged and processing continues.</p>"},{"location":"architecture/providers/","title":"Provider System","text":""},{"location":"architecture/providers/#overview","title":"Overview","text":"<p>PlanOpticon supports multiple AI providers through a unified abstraction layer. Default models favor cost-effective options (Haiku, GPT-4o-mini, Gemini Flash) for routine tasks, with more capable models available when needed.</p>"},{"location":"architecture/providers/#supported-providers","title":"Supported providers","text":"Provider Chat Vision Transcription Env Variable OpenAI GPT-4o-mini, GPT-4o GPT-4o-mini, GPT-4o Whisper-1 <code>OPENAI_API_KEY</code> Anthropic Claude Haiku, Sonnet, Opus Claude Haiku, Sonnet, Opus \u2014 <code>ANTHROPIC_API_KEY</code> Google Gemini Gemini Flash, Pro Gemini Flash, Pro Gemini Flash <code>GEMINI_API_KEY</code> Azure OpenAI GPT-4o-mini, GPT-4o GPT-4o-mini, GPT-4o Whisper-1 <code>AZURE_OPENAI_API_KEY</code>, <code>AZURE_OPENAI_ENDPOINT</code> Together AI Llama, Mixtral, etc. Llava \u2014 <code>TOGETHER_API_KEY</code> Fireworks AI Llama, Mixtral, etc. Llava \u2014 <code>FIREWORKS_API_KEY</code> Cerebras Llama (fast inference) \u2014 \u2014 <code>CEREBRAS_API_KEY</code> xAI Grok Grok \u2014 <code>XAI_API_KEY</code> Ollama (local) Any installed model llava, moondream, etc. \u2014 (use local Whisper) <code>OLLAMA_HOST</code>"},{"location":"architecture/providers/#default-models","title":"Default models","text":"<p>PlanOpticon defaults to cheap, fast models for cost efficiency:</p> Task Default model Vision (diagrams) Gemini Flash Chat (analysis) Claude Haiku Transcription Local Whisper (fallback: Whisper-1) <p>Use <code>--vision-model</code> and <code>--chat-model</code> to override with more capable models when needed (e.g., <code>--chat-model claude-sonnet-4-20250514</code> for complex analysis).</p>"},{"location":"architecture/providers/#ollama-offline-mode","title":"Ollama (offline mode)","text":"<p>Ollama enables fully offline operation with no API keys required. PlanOpticon connects via Ollama's OpenAI-compatible API.</p> <pre><code># Install and start Ollama\nollama serve\n\n# Pull a chat model\nollama pull llama3.2\n\n# Pull a vision model (for diagram analysis)\nollama pull llava\n</code></pre> <p>PlanOpticon auto-detects Ollama when it's running. To force Ollama:</p> <pre><code>planopticon analyze -i video.mp4 -o ./out --provider ollama\n</code></pre> <p>Configure a non-default host via <code>OLLAMA_HOST</code>:</p> <pre><code>export OLLAMA_HOST=http://192.168.1.100:11434\n</code></pre>"},{"location":"architecture/providers/#auto-discovery","title":"Auto-discovery","text":"<p>On startup, <code>ProviderManager</code> checks which API keys are configured, queries each provider's API, and checks for a running Ollama server to discover available models:</p> <pre><code>from video_processor.providers.manager import ProviderManager\n\npm = ProviderManager()\n# Automatically discovers models from all configured providers + Ollama\n</code></pre>"},{"location":"architecture/providers/#routing-preferences","title":"Routing preferences","text":"<p>Each task type has a default preference order (cheapest first):</p> Task Preference Vision Gemini Flash \u2192 GPT-4o-mini \u2192 Claude Haiku \u2192 Ollama Chat Claude Haiku \u2192 GPT-4o-mini \u2192 Gemini Flash \u2192 Ollama Transcription Local Whisper \u2192 Whisper-1 \u2192 Gemini Flash <p>Ollama acts as the last-resort fallback -- if no cloud API keys are set but Ollama is running, it is used automatically.</p>"},{"location":"architecture/providers/#manual-override","title":"Manual override","text":"<pre><code>pm = ProviderManager(\n vision_model=\"gpt-4o\",\n chat_model=\"claude-sonnet-4-20250514\",\n provider=\"openai\", # Force a specific provider\n)\n\n# Use a cheap model for bulk processing\npm = ProviderManager(\n chat_model=\"claude-haiku-3-5-20241022\",\n vision_model=\"gemini-2.0-flash\",\n)\n\n# Or use Ollama for fully offline processing\npm = ProviderManager(provider=\"ollama\")\n\n# Use Azure OpenAI\npm = ProviderManager(provider=\"azure\")\n\n# Use Together AI for open-source models\npm = ProviderManager(provider=\"together\", chat_model=\"meta-llama/Llama-3.3-70B-Instruct-Turbo\")\n</code></pre>"},{"location":"architecture/providers/#baseprovider-interface","title":"BaseProvider interface","text":"<p>All providers implement:</p> <pre><code>class BaseProvider(ABC):\n def chat(messages, max_tokens, temperature) -> str\n def analyze_image(image_path, prompt, max_tokens) -> str\n def transcribe_audio(audio_path) -> dict\n def list_models() -> List[ModelInfo]\n</code></pre>"},{"location":"getting-started/configuration/","title":"Configuration","text":""},{"location":"getting-started/configuration/#example-env-file","title":"Example <code>.env</code> file","text":"<p>Create a <code>.env</code> file in your project directory. PlanOpticon loads it automatically.</p> <pre><code># =============================================================================\n# PlanOpticon Configuration\n# =============================================================================\n# Copy this file to .env and fill in the values you need.\n# You only need ONE AI provider \u2014 PlanOpticon auto-detects which are available.\n\n# --- AI Providers (set at least one) ----------------------------------------\n\n# OpenAI \u2014 get your key at https://platform.openai.com/api-keys\nOPENAI_API_KEY=sk-...\n\n# Anthropic \u2014 get your key at https://console.anthropic.com/settings/keys\nANTHROPIC_API_KEY=sk-ant-...\n\n# Google Gemini \u2014 get your key at https://aistudio.google.com/apikey\nGEMINI_API_KEY=AI...\n\n# Azure OpenAI \u2014 from your Azure portal deployment\n# AZURE_OPENAI_API_KEY=...\n# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/\n\n# Together AI \u2014 https://api.together.xyz/settings/api-keys\n# TOGETHER_API_KEY=...\n\n# Fireworks AI \u2014 https://fireworks.ai/account/api-keys\n# FIREWORKS_API_KEY=...\n\n# Cerebras \u2014 https://cloud.cerebras.ai/\n# CEREBRAS_API_KEY=...\n\n# xAI (Grok) \u2014 https://console.x.ai/\n# XAI_API_KEY=...\n\n# Ollama (local, no key needed) \u2014 just run: ollama serve\n# OLLAMA_HOST=http://localhost:11434\n\n# --- Google (Drive, Docs, Sheets, Meet, YouTube) ----------------------------\n# Option A: OAuth (interactive, recommended for personal use)\n# Create credentials at https://console.cloud.google.com/apis/credentials\n# 1. Create an OAuth 2.0 Client ID (Desktop application)\n# 2. Enable these APIs: Google Drive API, Google Docs API\nGOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=GOCSPX-...\n\n# Option B: Service Account (automated/server-side)\n# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json\n\n# --- Zoom (recordings) ------------------------------------------------------\n# Create an OAuth app at https://marketplace.zoom.us/develop/create\n# App type: \"General App\" with OAuth\n# Scopes: cloud_recording:read:list_user_recordings, cloud_recording:read:recording\nZOOM_CLIENT_ID=...\nZOOM_CLIENT_SECRET=...\n# For Server-to-Server (no browser needed):\n# ZOOM_ACCOUNT_ID=...\n\n# --- Microsoft 365 (OneDrive, SharePoint, Teams) ----------------------------\n# Register an app at https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps\n# API permissions: OnlineMeetings.Read, Files.Read (delegated)\nMICROSOFT_CLIENT_ID=...\nMICROSOFT_CLIENT_SECRET=...\n\n# --- Notion ------------------------------------------------------------------\n# Option A: OAuth (create integration at https://www.notion.so/my-integrations)\n# NOTION_CLIENT_ID=...\n# NOTION_CLIENT_SECRET=...\n\n# Option B: API key (simpler, from the same integrations page)\nNOTION_API_KEY=secret_...\n\n# --- GitHub ------------------------------------------------------------------\n# Option A: Personal Access Token (simplest)\n# Create at https://github.com/settings/tokens \u2014 needs 'repo' scope\nGITHUB_TOKEN=ghp_...\n\n# Option B: OAuth App (for CI/automation)\n# GITHUB_CLIENT_ID=...\n# GITHUB_CLIENT_SECRET=...\n\n# --- Dropbox -----------------------------------------------------------------\n# Create an app at https://www.dropbox.com/developers/apps\n# DROPBOX_APP_KEY=...\n# DROPBOX_APP_SECRET=...\n# Or use a long-lived access token:\n# DROPBOX_ACCESS_TOKEN=...\n\n# --- General -----------------------------------------------------------------\n# CACHE_DIR=~/.cache/planopticon\n</code></pre>"},{"location":"getting-started/configuration/#environment-variables-reference","title":"Environment variables reference","text":""},{"location":"getting-started/configuration/#ai-providers","title":"AI providers","text":"Variable Required Where to get it <code>OPENAI_API_KEY</code> At least one provider platform.openai.com/api-keys <code>ANTHROPIC_API_KEY</code> At least one provider console.anthropic.com <code>GEMINI_API_KEY</code> At least one provider aistudio.google.com/apikey <code>AZURE_OPENAI_API_KEY</code> Optional Azure portal > your OpenAI resource <code>AZURE_OPENAI_ENDPOINT</code> With Azure Azure portal > your OpenAI resource <code>TOGETHER_API_KEY</code> Optional api.together.xyz <code>FIREWORKS_API_KEY</code> Optional fireworks.ai <code>CEREBRAS_API_KEY</code> Optional cloud.cerebras.ai <code>XAI_API_KEY</code> Optional console.x.ai <code>OLLAMA_HOST</code> Optional Default: <code>http://localhost:11434</code>"},{"location":"getting-started/configuration/#cloud-services","title":"Cloud services","text":"Variable Service Auth method <code>GOOGLE_CLIENT_ID</code> Google (Drive, Docs, Meet) OAuth <code>GOOGLE_CLIENT_SECRET</code> Google OAuth <code>GOOGLE_APPLICATION_CREDENTIALS</code> Google Service account <code>ZOOM_CLIENT_ID</code> Zoom OAuth <code>ZOOM_CLIENT_SECRET</code> Zoom OAuth <code>ZOOM_ACCOUNT_ID</code> Zoom Server-to-Server <code>MICROSOFT_CLIENT_ID</code> Microsoft 365 OAuth <code>MICROSOFT_CLIENT_SECRET</code> Microsoft 365 OAuth <code>NOTION_CLIENT_ID</code> Notion OAuth <code>NOTION_CLIENT_SECRET</code> Notion OAuth <code>NOTION_API_KEY</code> Notion API key <code>GITHUB_CLIENT_ID</code> GitHub OAuth <code>GITHUB_CLIENT_SECRET</code> GitHub OAuth <code>GITHUB_TOKEN</code> GitHub API key <code>DROPBOX_APP_KEY</code> Dropbox OAuth <code>DROPBOX_APP_SECRET</code> Dropbox OAuth <code>DROPBOX_ACCESS_TOKEN</code> Dropbox API key"},{"location":"getting-started/configuration/#general","title":"General","text":"Variable Description <code>CACHE_DIR</code> Directory for API response caching"},{"location":"getting-started/configuration/#authentication","title":"Authentication","text":"<p>PlanOpticon uses OAuth for cloud services. Run <code>planopticon auth</code> once per service \u2014 tokens are saved locally and refreshed automatically.</p> <pre><code>planopticon auth google # Google Drive, Docs, Meet, YouTube\nplanopticon auth dropbox # Dropbox\nplanopticon auth zoom # Zoom recordings\nplanopticon auth notion # Notion pages\nplanopticon auth github # GitHub repos and wikis\nplanopticon auth microsoft # OneDrive, SharePoint, Teams\n</code></pre> <p>Credentials are stored in <code>~/.planopticon/</code>. Use <code>planopticon auth SERVICE --logout</code> to remove them.</p>"},{"location":"getting-started/configuration/#what-each-service-needs","title":"What each service needs","text":"Service Minimum setup Full OAuth setup Google <code>GOOGLE_CLIENT_ID</code> + <code>GOOGLE_CLIENT_SECRET</code> Create OAuth credentials in Google Cloud Console Zoom <code>ZOOM_CLIENT_ID</code> + <code>ZOOM_CLIENT_SECRET</code> Create a General App at marketplace.zoom.us Microsoft <code>MICROSOFT_CLIENT_ID</code> + <code>MICROSOFT_CLIENT_SECRET</code> Register app in Azure AD Notion <code>NOTION_API_KEY</code> (simplest) Create integration at notion.so/my-integrations GitHub <code>GITHUB_TOKEN</code> (simplest) Create token at github.com/settings/tokens Dropbox <code>DROPBOX_APP_KEY</code> + <code>DROPBOX_APP_SECRET</code> Create app at dropbox.com/developers <p>For detailed OAuth app creation walkthroughs, see the Authentication guide.</p>"},{"location":"getting-started/configuration/#provider-routing","title":"Provider routing","text":"<p>PlanOpticon auto-discovers available models and routes each task to the cheapest capable option:</p> Task Default preference Vision (diagrams) Gemini Flash > GPT-4o-mini > Claude Haiku > Ollama Chat (analysis) Claude Haiku > GPT-4o-mini > Gemini Flash > Ollama Transcription Local Whisper > Whisper-1 > Gemini Flash <p>Default models prioritize cost efficiency. For complex or high-stakes analysis, override with more capable models using <code>--chat-model</code> or <code>--vision-model</code>.</p> <p>If no cloud API keys are configured, PlanOpticon automatically falls back to Ollama when a local server is running. This enables fully offline operation when paired with local Whisper for transcription.</p> <p>Override with <code>--provider</code>, <code>--vision-model</code>, or <code>--chat-model</code> flags.</p>"},{"location":"getting-started/configuration/#frame-sampling","title":"Frame sampling","text":"<p>Control how frames are extracted:</p> <pre><code># Sample rate: frames per second (default: 0.5)\nplanopticon analyze -i video.mp4 -o ./out --sampling-rate 1.0\n\n# Change threshold: visual difference needed to keep a frame (default: 0.15)\nplanopticon analyze -i video.mp4 -o ./out --change-threshold 0.1\n\n# Periodic capture: capture a frame every N seconds regardless of change (default: 30)\n# Useful for slow-evolving content like document scrolling\nplanopticon analyze -i video.mp4 -o ./out --periodic-capture 15\n\n# Disable periodic capture (rely only on change detection)\nplanopticon analyze -i video.mp4 -o ./out --periodic-capture 0\n</code></pre> <p>Lower <code>change-threshold</code> = more frames kept. Higher <code>sampling-rate</code> = more candidates. Periodic capture catches content that changes too slowly for change detection (e.g., scrolling through a document during a screen share).</p> <p>People/webcam frames are automatically filtered out using face detection \u2014 no configuration needed.</p>"},{"location":"getting-started/configuration/#focus-areas","title":"Focus areas","text":"<p>Limit processing to specific extraction types:</p> <pre><code>planopticon analyze -i video.mp4 -o ./out --focus \"diagrams,action-items\"\n</code></pre>"},{"location":"getting-started/configuration/#gpu-acceleration","title":"GPU acceleration","text":"<pre><code>planopticon analyze -i video.mp4 -o ./out --use-gpu\n</code></pre> <p>Requires <code>planopticon[gpu]</code> extras installed.</p>"},{"location":"getting-started/installation/","title":"Installation","text":""},{"location":"getting-started/installation/#from-pypi","title":"From PyPI","text":"<pre><code>pip install planopticon\n</code></pre>"},{"location":"getting-started/installation/#optional-extras","title":"Optional extras","text":"<pre><code># PDF export support\npip install planopticon[pdf]\n\n# Google Drive + Dropbox integration\npip install planopticon[cloud]\n\n# GPU acceleration\npip install planopticon[gpu]\n\n# Everything\npip install planopticon[all]\n</code></pre>"},{"location":"getting-started/installation/#from-source","title":"From source","text":"<pre><code>git clone https://github.com/ConflictHQ/PlanOpticon.git\ncd PlanOpticon\npip install -e \".[dev]\"\n</code></pre>"},{"location":"getting-started/installation/#binary-download","title":"Binary download","text":"<p>Download standalone binaries (no Python required) from GitHub Releases:</p> Platform Download macOS (Apple Silicon) <code>planopticon-macos-arm64</code> macOS (Intel) <code>planopticon-macos-x86_64</code> Linux (x86_64) <code>planopticon-linux-x86_64</code> Windows <code>planopticon-windows-x86_64.exe</code>"},{"location":"getting-started/installation/#system-dependencies","title":"System dependencies","text":"<p>PlanOpticon requires FFmpeg for audio extraction:</p> macOSUbuntu/DebianWindows <pre><code>brew install ffmpeg\n</code></pre> <pre><code>sudo apt-get install ffmpeg libsndfile1\n</code></pre> <p>Download from ffmpeg.org and add to PATH.</p>"},{"location":"getting-started/installation/#api-keys","title":"API keys","text":"<p>You need at least one AI provider API key or a running Ollama server.</p>"},{"location":"getting-started/installation/#cloud-providers","title":"Cloud providers","text":"<p>Set API keys as environment variables:</p> <pre><code>export OPENAI_API_KEY=\"sk-...\"\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\nexport GEMINI_API_KEY=\"AI...\"\n</code></pre> <p>Or create a <code>.env</code> file in your project directory:</p> <pre><code>OPENAI_API_KEY=sk-...\nANTHROPIC_API_KEY=sk-ant-...\nGEMINI_API_KEY=AI...\n</code></pre>"},{"location":"getting-started/installation/#ollama-fully-offline","title":"Ollama (fully offline)","text":"<p>No API keys needed \u2014 just install and run Ollama:</p> <pre><code># Install Ollama, then pull models\nollama pull llama3.2 # Chat/analysis\nollama pull llava # Vision (diagram detection)\n\n# Start the server (if not already running)\nollama serve\n</code></pre> <p>PlanOpticon auto-detects Ollama and uses it as a fallback when no cloud API keys are set. For a fully offline pipeline, pair Ollama with local Whisper transcription (<code>pip install planopticon[gpu]</code>).</p> <p>PlanOpticon will automatically discover which providers are available and route to the best model for each task.</p>"},{"location":"getting-started/quickstart/","title":"Quick Start","text":""},{"location":"getting-started/quickstart/#analyze-a-single-video","title":"Analyze a single video","text":"<pre><code>planopticon analyze -i meeting.mp4 -o ./output\n</code></pre> <p>This runs the full pipeline:</p> <ol> <li>Extracts video frames (smart sampling, change detection)</li> <li>Extracts and transcribes audio</li> <li>Detects and analyzes diagrams, charts, whiteboards</li> <li>Builds a knowledge graph of entities and relationships</li> <li>Extracts key points and action items</li> <li>Generates markdown, HTML, and PDF reports</li> <li>Outputs a <code>manifest.json</code> with everything</li> </ol>"},{"location":"getting-started/quickstart/#processing-depth","title":"Processing depth","text":"<pre><code># Quick scan \u2014 transcription + key points only\nplanopticon analyze -i video.mp4 -o ./out --depth basic\n\n# Standard \u2014 includes diagram extraction (default)\nplanopticon analyze -i video.mp4 -o ./out --depth standard\n\n# Deep \u2014 more frames analyzed, richer extraction\nplanopticon analyze -i video.mp4 -o ./out --depth comprehensive\n</code></pre>"},{"location":"getting-started/quickstart/#choose-a-provider","title":"Choose a provider","text":"<pre><code># Auto-detect best available (default)\nplanopticon analyze -i video.mp4 -o ./out\n\n# Force a specific provider\nplanopticon analyze -i video.mp4 -o ./out --provider openai\n\n# Use Ollama for fully offline processing (no API keys needed)\nplanopticon analyze -i video.mp4 -o ./out --provider ollama\n\n# Override specific models\nplanopticon analyze -i video.mp4 -o ./out \\\n --vision-model gpt-4o \\\n --chat-model claude-sonnet-4-5-20250929\n</code></pre>"},{"location":"getting-started/quickstart/#batch-processing","title":"Batch processing","text":"<pre><code># Process all videos in a folder\nplanopticon batch -i ./recordings -o ./output\n\n# Custom file patterns\nplanopticon batch -i ./recordings -o ./output --pattern \"*.mp4,*.mov\"\n\n# With a title for the batch report\nplanopticon batch -i ./recordings -o ./output --title \"Q4 Sprint Reviews\"\n</code></pre> <p>Batch mode produces per-video outputs plus:</p> <ul> <li>Merged knowledge graph across all videos</li> <li>Batch summary with aggregated action items</li> <li>Cross-referenced entities</li> </ul>"},{"location":"getting-started/quickstart/#ingest-documents","title":"Ingest documents","text":"<p>Build a knowledge graph from documents, notes, or any text content:</p> <pre><code># Ingest a single file\nplanopticon ingest ./meeting-notes.md --output ./kb\n\n# Ingest a directory recursively\nplanopticon ingest ./docs/ --output ./kb --recursive\n\n# Ingest from a URL\nplanopticon ingest \"https://www.youtube.com/watch?v=example\" --output ./kb\n</code></pre>"},{"location":"getting-started/quickstart/#companion-repl","title":"Companion REPL","text":"<p>Chat with your knowledge base interactively:</p> <pre><code># Start the companion\nplanopticon companion --kb ./kb\n\n# Use a specific provider\nplanopticon companion --kb ./kb --provider anthropic\n</code></pre> <p>The companion understands your knowledge graph and can answer questions, find connections, and summarize topics conversationally.</p>"},{"location":"getting-started/quickstart/#planning-agent","title":"Planning agent","text":"<p>Run the planning agent for adaptive, goal-directed analysis:</p> <pre><code># Interactive mode \u2014 the agent asks before each action\nplanopticon agent --kb ./kb --interactive\n\n# Non-interactive with export\nplanopticon agent --kb ./kb --export ./plan.md\n</code></pre>"},{"location":"getting-started/quickstart/#query-the-knowledge-graph","title":"Query the knowledge graph","text":"<p>Query your knowledge graph directly without an AI provider:</p> <pre><code># Show graph stats (entity/relationship counts)\nplanopticon query stats\n\n# List entities by type\nplanopticon query \"entities --type technology\"\n\n# Find neighbors of an entity\nplanopticon query \"neighbors Alice\"\n\n# Natural language query (requires API key)\nplanopticon query \"What technologies were discussed?\"\n\n# Interactive REPL\nplanopticon query -I\n</code></pre>"},{"location":"getting-started/quickstart/#export","title":"Export","text":"<p>Export your knowledge base to various formats:</p> <pre><code># Export to Markdown files\nplanopticon export markdown --input ./kb --output ./docs\n\n# Export to an Obsidian vault\nplanopticon export obsidian --input ./kb --output ~/Obsidian/PlanOpticon\n\n# Export to Notion\nplanopticon export notion --input ./kb --parent-page abc123\n\n# Export as exchange format (portable JSON)\nplanopticon export exchange --input ./kb --output ./export.json\n</code></pre>"},{"location":"getting-started/quickstart/#discover-available-models","title":"Discover available models","text":"<pre><code>planopticon list-models\n</code></pre> <p>Shows all models from configured providers with their capabilities (vision, chat, transcription).</p>"},{"location":"getting-started/quickstart/#output-structure","title":"Output structure","text":"<p>After processing, your output directory looks like:</p> <pre><code>output/\n\u251c\u2500\u2500 manifest.json # Single source of truth\n\u251c\u2500\u2500 transcript/\n\u2502 \u251c\u2500\u2500 transcript.json # Full transcript with segments\n\u2502 \u251c\u2500\u2500 transcript.txt # Plain text\n\u2502 \u2514\u2500\u2500 transcript.srt # Subtitles\n\u251c\u2500\u2500 frames/ # Extracted video frames\n\u251c\u2500\u2500 diagrams/ # Detected diagrams + mermaid/SVG/PNG\n\u251c\u2500\u2500 captures/ # Screengrab fallbacks\n\u2514\u2500\u2500 results/\n \u251c\u2500\u2500 analysis.md # Markdown report\n \u251c\u2500\u2500 analysis.html # HTML report\n \u251c\u2500\u2500 analysis.pdf # PDF report\n \u251c\u2500\u2500 knowledge_graph.json\n \u251c\u2500\u2500 key_points.json\n \u2514\u2500\u2500 action_items.json\n</code></pre>"},{"location":"guide/authentication/","title":"Authentication","text":"<p>PlanOpticon uses a unified authentication system to connect with cloud services for fetching recordings, documents, and other content. The system is OAuth-first: it prefers OAuth 2.0 flows for security and token management, but falls back to API keys when OAuth is not configured.</p>"},{"location":"guide/authentication/#auth-strategy-overview","title":"Auth strategy overview","text":"<p>PlanOpticon supports six cloud services out of the box: Google, Dropbox, Zoom, Notion, GitHub, and Microsoft. Each service uses the same authentication chain, implemented through the <code>OAuthManager</code> class. You configure credentials once (via environment variables or directly), and PlanOpticon handles token acquisition, storage, refresh, and fallback automatically.</p> <p>All authentication state is managed through the <code>planopticon auth</code> CLI command, the <code>/auth</code> companion REPL command, or programmatically via the Python API.</p>"},{"location":"guide/authentication/#the-auth-chain","title":"The auth chain","text":"<p>When you authenticate with a service, PlanOpticon tries the following methods in order. It stops at the first one that succeeds:</p> <ol> <li> <p>Saved token -- Checks <code>~/.planopticon/{service}_token.json</code> for a previously saved token. If the token has not expired, it is used immediately. If it has expired but a refresh token is available, PlanOpticon attempts an automatic token refresh.</p> </li> <li> <p>Client Credentials grant (Server-to-Server) -- If an <code>account_id</code> is configured (e.g., <code>ZOOM_ACCOUNT_ID</code>), PlanOpticon attempts a client credentials grant. This is a non-interactive flow suitable for automated pipelines and server-side integrations. No browser is required.</p> </li> <li> <p>OAuth 2.0 Authorization Code with PKCE (interactive) -- If a client ID is configured and OAuth endpoints are available, PlanOpticon initiates an interactive OAuth PKCE flow. It opens a browser to the service's authorization page, waits for you to paste the authorization code, and exchanges it for tokens. The tokens are saved for future use.</p> </li> <li> <p>API key fallback -- If no OAuth method succeeds, PlanOpticon checks for a service-specific API key environment variable (e.g., <code>GITHUB_TOKEN</code>, <code>NOTION_API_KEY</code>). This is the simplest setup but may have reduced capabilities compared to OAuth.</p> </li> </ol> <p>If none of the four methods succeed, PlanOpticon returns an error with hints about which environment variables to set.</p>"},{"location":"guide/authentication/#token-storage","title":"Token storage","text":"<p>Tokens are persisted as JSON files in <code>~/.planopticon/</code>:</p> <pre><code>~/.planopticon/\n google_token.json\n dropbox_token.json\n zoom_token.json\n notion_token.json\n github_token.json\n microsoft_token.json\n</code></pre> <p>Each token file contains:</p> Field Description <code>access_token</code> The current access token <code>refresh_token</code> Refresh token for automatic renewal (if provided by the service) <code>expires_at</code> Unix timestamp when the token expires (with a 60-second safety margin) <code>client_id</code> The client ID used for this token (for refresh) <code>client_secret</code> The client secret used (for refresh) <p>The <code>~/.planopticon/</code> directory is created automatically on first use. Token files are overwritten on each successful authentication or refresh.</p> <p>To remove a saved token, use <code>planopticon auth <service> --logout</code> or delete the file directly.</p>"},{"location":"guide/authentication/#supported-services","title":"Supported services","text":""},{"location":"guide/authentication/#google","title":"Google","text":"<p>Google authentication provides access to Google Drive and Google Docs for fetching documents, recordings, and other content.</p> <p>Scopes requested:</p> <ul> <li><code>https://www.googleapis.com/auth/drive.readonly</code></li> <li><code>https://www.googleapis.com/auth/documents.readonly</code></li> </ul> <p>Environment variables:</p> Variable Required Description <code>GOOGLE_CLIENT_ID</code> For OAuth OAuth 2.0 Client ID from Google Cloud Console <code>GOOGLE_CLIENT_SECRET</code> For OAuth OAuth 2.0 Client Secret <code>GOOGLE_API_KEY</code> Fallback API key (limited access, no user-specific data) <p>OAuth app setup:</p> <ol> <li>Go to the Google Cloud Console.</li> <li>Create a project (or select an existing one).</li> <li>Navigate to APIs & Services > Credentials.</li> <li>Click Create Credentials > OAuth client ID.</li> <li>Choose Desktop app as the application type.</li> <li>Copy the Client ID and Client Secret.</li> <li>Under APIs & Services > Library, enable the Google Drive API and Google Docs API.</li> <li>Set the environment variables:</li> </ol> <pre><code>export GOOGLE_CLIENT_ID=\"your-client-id.apps.googleusercontent.com\"\nexport GOOGLE_CLIENT_SECRET=\"your-client-secret\"\n</code></pre> <p>Service account fallback: For automated pipelines, you can use a Google service account instead of OAuth. Generate a service account key JSON file from the Google Cloud Console and set <code>GOOGLE_APPLICATION_CREDENTIALS</code> to point to it. The PlanOpticon Google Workspace connector (<code>planopticon gws</code>) uses the <code>gws</code> CLI which has its own auth flow via <code>gws auth login</code>.</p>"},{"location":"guide/authentication/#dropbox","title":"Dropbox","text":"<p>Dropbox authentication provides access to files stored in Dropbox.</p> <p>Environment variables:</p> Variable Required Description <code>DROPBOX_APP_KEY</code> For OAuth App key from the Dropbox App Console <code>DROPBOX_APP_SECRET</code> For OAuth App secret <code>DROPBOX_ACCESS_TOKEN</code> Fallback Long-lived access token (for quick setup) <p>OAuth app setup:</p> <ol> <li>Go to the Dropbox App Console.</li> <li>Click Create App.</li> <li>Choose Scoped access and Full Dropbox (or App folder for restricted access).</li> <li>Copy the App key and App secret from the Settings tab.</li> <li>Set the environment variables:</li> </ol> <pre><code>export DROPBOX_APP_KEY=\"your-app-key\"\nexport DROPBOX_APP_SECRET=\"your-app-secret\"\n</code></pre> <p>Access token shortcut: For quick testing, you can generate an access token directly from the app's Settings page in the Dropbox App Console and set it as <code>DROPBOX_ACCESS_TOKEN</code>. This bypasses OAuth entirely but the token may have a limited lifetime.</p>"},{"location":"guide/authentication/#zoom","title":"Zoom","text":"<p>Zoom authentication provides access to cloud recordings, meeting metadata, and transcripts.</p> <p>Environment variables:</p> Variable Required Description <code>ZOOM_CLIENT_ID</code> For OAuth OAuth client ID from the Zoom Marketplace <code>ZOOM_CLIENT_SECRET</code> For OAuth OAuth client secret <code>ZOOM_ACCOUNT_ID</code> For S2S Account ID for Server-to-Server OAuth <p>Server-to-Server (recommended for automation):</p> <p>When <code>ZOOM_ACCOUNT_ID</code> is set alongside <code>ZOOM_CLIENT_ID</code> and <code>ZOOM_CLIENT_SECRET</code>, PlanOpticon uses the client credentials grant (Server-to-Server OAuth). This is non-interactive and ideal for CI/CD pipelines and scheduled jobs.</p> <ol> <li>Go to the Zoom Marketplace.</li> <li>Click Develop > Build App.</li> <li>Choose Server-to-Server OAuth.</li> <li>Copy the Account ID, Client ID, and Client Secret.</li> <li>Add the required scopes: <code>recording:read:admin</code> (or <code>recording:read</code>).</li> <li>Set the environment variables:</li> </ol> <pre><code>export ZOOM_CLIENT_ID=\"your-client-id\"\nexport ZOOM_CLIENT_SECRET=\"your-client-secret\"\nexport ZOOM_ACCOUNT_ID=\"your-account-id\"\n</code></pre> <p>User-level OAuth PKCE:</p> <p>If <code>ZOOM_ACCOUNT_ID</code> is not set, PlanOpticon falls back to the interactive OAuth PKCE flow. This opens a browser window for the user to authorize access.</p> <ol> <li>In the Zoom Marketplace, create a General App (or OAuth app).</li> <li>Set the redirect URI to <code>urn:ietf:wg:oauth:2.0:oob</code> (out-of-band).</li> <li>Copy the Client ID and Client Secret.</li> </ol>"},{"location":"guide/authentication/#notion","title":"Notion","text":"<p>Notion authentication provides access to pages, databases, and content in your Notion workspace.</p> <p>Environment variables:</p> Variable Required Description <code>NOTION_CLIENT_ID</code> For OAuth OAuth client ID from the Notion Integrations page <code>NOTION_CLIENT_SECRET</code> For OAuth OAuth client secret <code>NOTION_API_KEY</code> Fallback Internal integration token <p>OAuth app setup:</p> <ol> <li>Go to My Integrations in Notion.</li> <li>Click New integration.</li> <li>Select Public integration (required for OAuth).</li> <li>Copy the OAuth Client ID and Client Secret.</li> <li>Set the redirect URI.</li> <li>Set the environment variables:</li> </ol> <pre><code>export NOTION_CLIENT_ID=\"your-client-id\"\nexport NOTION_CLIENT_SECRET=\"your-client-secret\"\n</code></pre> <p>Internal integration (API key fallback):</p> <p>For simpler setups, create an Internal integration from the Notion Integrations page. Copy the integration token and set it as <code>NOTION_API_KEY</code>. You must also share the relevant Notion pages/databases with the integration.</p> <pre><code>export NOTION_API_KEY=\"ntn_your-integration-token\"\n</code></pre>"},{"location":"guide/authentication/#github","title":"GitHub","text":"<p>GitHub authentication provides access to repositories, issues, and organization data.</p> <p>Scopes requested:</p> <ul> <li><code>repo</code></li> <li><code>read:org</code></li> </ul> <p>Environment variables:</p> Variable Required Description <code>GITHUB_CLIENT_ID</code> For OAuth OAuth App client ID <code>GITHUB_CLIENT_SECRET</code> For OAuth OAuth App client secret <code>GITHUB_TOKEN</code> Fallback Personal access token (classic or fine-grained) <p>OAuth app setup:</p> <ol> <li>Go to GitHub > Settings > Developer Settings > OAuth Apps.</li> <li>Click New OAuth App.</li> <li>Set the Authorization callback URL to <code>urn:ietf:wg:oauth:2.0:oob</code>.</li> <li>Copy the Client ID and generate a Client Secret.</li> <li>Set the environment variables:</li> </ol> <pre><code>export GITHUB_CLIENT_ID=\"your-client-id\"\nexport GITHUB_CLIENT_SECRET=\"your-client-secret\"\n</code></pre> <p>Personal access token (recommended for most users):</p> <p>The simplest approach is to create a Personal Access Token:</p> <ol> <li>Go to GitHub > Settings > Developer Settings > Personal Access Tokens.</li> <li>Generate a token with <code>repo</code> and <code>read:org</code> scopes.</li> <li>Set it as <code>GITHUB_TOKEN</code>:</li> </ol> <pre><code>export GITHUB_TOKEN=\"ghp_your-token\"\n</code></pre>"},{"location":"guide/authentication/#microsoft","title":"Microsoft","text":"<p>Microsoft authentication provides access to Microsoft 365 resources via the Microsoft Graph API, including OneDrive, SharePoint, and Teams recordings.</p> <p>Scopes requested:</p> <ul> <li><code>https://graph.microsoft.com/OnlineMeetings.Read</code></li> <li><code>https://graph.microsoft.com/Files.Read</code></li> </ul> <p>Environment variables:</p> Variable Required Description <code>MICROSOFT_CLIENT_ID</code> For OAuth Application (client) ID from Azure AD <code>MICROSOFT_CLIENT_SECRET</code> For OAuth Client secret from Azure AD <p>Azure AD app registration:</p> <ol> <li>Go to the Azure Portal.</li> <li>Navigate to Azure Active Directory > App registrations.</li> <li>Click New registration.</li> <li>Name the application (e.g., \"PlanOpticon\").</li> <li>Under Supported account types, select the appropriate option for your organization.</li> <li>Set the redirect URI to <code>urn:ietf:wg:oauth:2.0:oob</code> with platform Mobile and desktop applications.</li> <li>After registration, go to Certificates & secrets and create a new client secret.</li> <li>Under API permissions, add:<ul> <li><code>OnlineMeetings.Read</code></li> <li><code>Files.Read</code></li> </ul> </li> <li>Grant admin consent if required by your organization.</li> <li>Set the environment variables:</li> </ol> <pre><code>export MICROSOFT_CLIENT_ID=\"your-application-id\"\nexport MICROSOFT_CLIENT_SECRET=\"your-client-secret\"\n</code></pre> <p>Microsoft 365 CLI: The <code>planopticon m365</code> commands use the <code>@pnp/cli-microsoft365</code> npm package, which has its own authentication flow via <code>m365 login</code>. This is separate from the OAuth flow described above.</p>"},{"location":"guide/authentication/#cli-usage","title":"CLI usage","text":""},{"location":"guide/authentication/#planopticon-auth","title":"<code>planopticon auth</code>","text":"<p>Authenticate with a cloud service or manage saved tokens.</p> <pre><code>planopticon auth SERVICE [--logout]\n</code></pre> <p>Arguments:</p> Argument Description <code>SERVICE</code> One of: <code>google</code>, <code>dropbox</code>, <code>zoom</code>, <code>notion</code>, <code>github</code>, <code>microsoft</code> <p>Options:</p> Option Description <code>--logout</code> Clear the saved token for the specified service <p>Examples:</p> <pre><code># Authenticate with Google (triggers OAuth flow or uses saved token)\nplanopticon auth google\n\n# Authenticate with Zoom\nplanopticon auth zoom\n\n# Clear saved GitHub token\nplanopticon auth github --logout\n</code></pre> <p>On success, the command prints the authentication method used:</p> <pre><code>Google authentication successful (oauth_pkce).\n</code></pre> <p>or</p> <pre><code>Github authentication successful (api_key).\n</code></pre>"},{"location":"guide/authentication/#companion-repl-auth","title":"Companion REPL <code>/auth</code>","text":"<p>Inside the interactive companion REPL (<code>planopticon -C</code> or <code>planopticon -I</code>), you can authenticate with services using the <code>/auth</code> command:</p> <pre><code>/auth SERVICE\n</code></pre> <p>Without arguments, <code>/auth</code> lists all available services:</p> <pre><code>> /auth\nUsage: /auth SERVICE\nAvailable: dropbox, github, google, microsoft, notion, zoom\n</code></pre> <p>With a service name, it runs the same auth chain as the CLI command:</p> <pre><code>> /auth github\nGithub authentication successful (api_key).\n</code></pre>"},{"location":"guide/authentication/#environment-variables-reference","title":"Environment variables reference","text":"<p>The following table summarizes all environment variables used by the authentication system:</p> Service OAuth Client ID OAuth Client Secret API Key / Token Account ID Google <code>GOOGLE_CLIENT_ID</code> <code>GOOGLE_CLIENT_SECRET</code> <code>GOOGLE_API_KEY</code> -- Dropbox <code>DROPBOX_APP_KEY</code> <code>DROPBOX_APP_SECRET</code> <code>DROPBOX_ACCESS_TOKEN</code> -- Zoom <code>ZOOM_CLIENT_ID</code> <code>ZOOM_CLIENT_SECRET</code> -- <code>ZOOM_ACCOUNT_ID</code> Notion <code>NOTION_CLIENT_ID</code> <code>NOTION_CLIENT_SECRET</code> <code>NOTION_API_KEY</code> -- GitHub <code>GITHUB_CLIENT_ID</code> <code>GITHUB_CLIENT_SECRET</code> <code>GITHUB_TOKEN</code> -- Microsoft <code>MICROSOFT_CLIENT_ID</code> <code>MICROSOFT_CLIENT_SECRET</code> -- --"},{"location":"guide/authentication/#python-api","title":"Python API","text":""},{"location":"guide/authentication/#authconfig","title":"AuthConfig","text":"<p>The <code>AuthConfig</code> dataclass defines the authentication configuration for a service. It holds OAuth endpoints, credential references, scopes, and token storage paths.</p> <pre><code>from video_processor.auth import AuthConfig\n\nconfig = AuthConfig(\n service=\"myservice\",\n oauth_authorize_url=\"https://example.com/oauth/authorize\",\n oauth_token_url=\"https://example.com/oauth/token\",\n client_id_env=\"MYSERVICE_CLIENT_ID\",\n client_secret_env=\"MYSERVICE_CLIENT_SECRET\",\n api_key_env=\"MYSERVICE_API_KEY\",\n scopes=[\"read\", \"write\"],\n)\n</code></pre> <p>Key fields:</p> Field Type Description <code>service</code> <code>str</code> Service identifier (used for token filename) <code>oauth_authorize_url</code> <code>Optional[str]</code> OAuth authorization endpoint <code>oauth_token_url</code> <code>Optional[str]</code> OAuth token endpoint <code>client_id</code> / <code>client_id_env</code> <code>Optional[str]</code> Client ID value or env var name <code>client_secret</code> / <code>client_secret_env</code> <code>Optional[str]</code> Client secret value or env var name <code>api_key_env</code> <code>Optional[str]</code> Environment variable for API key fallback <code>scopes</code> <code>List[str]</code> OAuth scopes to request <code>redirect_uri</code> <code>str</code> Redirect URI (default: <code>urn:ietf:wg:oauth:2.0:oob</code>) <code>account_id</code> / <code>account_id_env</code> <code>Optional[str]</code> Account ID for client credentials grant <code>token_path</code> <code>Optional[Path]</code> Override token storage path <p>Resolved properties:</p> <ul> <li><code>resolved_client_id</code> -- Returns the client ID from the direct value or environment variable.</li> <li><code>resolved_client_secret</code> -- Returns the client secret from the direct value or environment variable.</li> <li><code>resolved_api_key</code> -- Returns the API key from the environment variable.</li> <li><code>resolved_account_id</code> -- Returns the account ID from the direct value or environment variable.</li> <li><code>resolved_token_path</code> -- Returns the token file path (default: <code>~/.planopticon/{service}_token.json</code>).</li> <li><code>supports_oauth</code> -- Returns <code>True</code> if both OAuth endpoints are configured.</li> </ul>"},{"location":"guide/authentication/#oauthmanager","title":"OAuthManager","text":"<p>The <code>OAuthManager</code> class manages the full authentication lifecycle for a service.</p> <pre><code>from video_processor.auth import OAuthManager, AuthConfig\n\nconfig = AuthConfig(\n service=\"notion\",\n oauth_authorize_url=\"https://api.notion.com/v1/oauth/authorize\",\n oauth_token_url=\"https://api.notion.com/v1/oauth/token\",\n client_id_env=\"NOTION_CLIENT_ID\",\n client_secret_env=\"NOTION_CLIENT_SECRET\",\n api_key_env=\"NOTION_API_KEY\",\n scopes=[\"read_content\"],\n)\nmanager = OAuthManager(config)\n\n# Full auth chain -- returns AuthResult\nresult = manager.authenticate()\nif result.success:\n print(f\"Authenticated via {result.method}\")\n print(f\"Token: {result.access_token[:20]}...\")\n\n# Convenience method -- returns just the token string or None\ntoken = manager.get_token()\n\n# Clear saved token (logout)\nmanager.clear_token()\n</code></pre> <p>AuthResult fields:</p> Field Type Description <code>success</code> <code>bool</code> Whether authentication succeeded <code>access_token</code> <code>Optional[str]</code> The access token (if successful) <code>method</code> <code>Optional[str]</code> One of: <code>saved_token</code>, <code>oauth_pkce</code>, <code>client_credentials</code>, <code>api_key</code> <code>expires_at</code> <code>Optional[float]</code> Token expiry as a Unix timestamp <code>refresh_token</code> <code>Optional[str]</code> Refresh token (if provided) <code>error</code> <code>Optional[str]</code> Error message (if unsuccessful)"},{"location":"guide/authentication/#pre-built-configs","title":"Pre-built configs","text":"<p>PlanOpticon ships with pre-built <code>AuthConfig</code> instances for all six supported services. Access them via convenience functions:</p> <pre><code>from video_processor.auth import get_auth_config, get_auth_manager\n\n# Get just the config\nconfig = get_auth_config(\"zoom\")\n\n# Get a ready-to-use manager\nmanager = get_auth_manager(\"github\")\ntoken = manager.get_token()\n</code></pre>"},{"location":"guide/authentication/#building-custom-connectors","title":"Building custom connectors","text":"<p>To add authentication for a new service, create an <code>AuthConfig</code> with the service's OAuth endpoints and credential environment variables:</p> <pre><code>from video_processor.auth import AuthConfig, OAuthManager\n\nconfig = AuthConfig(\n service=\"slack\",\n oauth_authorize_url=\"https://slack.com/oauth/v2/authorize\",\n oauth_token_url=\"https://slack.com/api/oauth.v2.access\",\n client_id_env=\"SLACK_CLIENT_ID\",\n client_secret_env=\"SLACK_CLIENT_SECRET\",\n api_key_env=\"SLACK_BOT_TOKEN\",\n scopes=[\"channels:read\", \"channels:history\"],\n)\n\nmanager = OAuthManager(config)\nresult = manager.authenticate()\n</code></pre> <p>The token will be saved to <code>~/.planopticon/slack_token.json</code> and automatically refreshed on subsequent calls.</p>"},{"location":"guide/authentication/#troubleshooting","title":"Troubleshooting","text":""},{"location":"guide/authentication/#no-auth-method-available-for-service","title":"\"No auth method available for {service}\"","text":"<p>This means none of the four auth methods succeeded. Check that:</p> <ul> <li>The required environment variables are set and non-empty.</li> <li>For OAuth: both the client ID and client secret (or app key/secret) are set.</li> <li>For API key fallback: the correct environment variable is set.</li> </ul> <p>The error message includes hints about which variables to set.</p>"},{"location":"guide/authentication/#token-refresh-fails","title":"Token refresh fails","text":"<p>If automatic token refresh fails, PlanOpticon falls back to the next auth method in the chain. Common causes:</p> <ul> <li>The refresh token has been revoked (e.g., you changed your password or revoked app access).</li> <li>The OAuth app's client secret has changed.</li> <li>The service requires re-authorization after a certain period.</li> </ul> <p>To resolve, clear the token and re-authenticate:</p> <pre><code>planopticon auth google --logout\nplanopticon auth google\n</code></pre>"},{"location":"guide/authentication/#oauth-pkce-flow-does-not-open-a-browser","title":"OAuth PKCE flow does not open a browser","text":"<p>If the browser does not open automatically, PlanOpticon prints the authorization URL to the terminal. Copy and paste it into your browser manually. After authorizing, paste the authorization code back into the terminal prompt.</p>"},{"location":"guide/authentication/#requests-not-installed","title":"\"requests not installed\"","text":"<p>The OAuth flows require the <code>requests</code> library. It is included as a dependency of PlanOpticon, but if you installed PlanOpticon in a minimal environment, install it manually:</p> <pre><code>pip install requests\n</code></pre>"},{"location":"guide/authentication/#permission-denied-on-token-file","title":"Permission denied on token file","text":"<p>PlanOpticon needs write access to <code>~/.planopticon/</code>. If the directory or token files have restrictive permissions, adjust them:</p> <pre><code>chmod 700 ~/.planopticon\nchmod 600 ~/.planopticon/*_token.json\n</code></pre>"},{"location":"guide/authentication/#microsoft-authentication-uses-the-common-tenant","title":"Microsoft authentication uses the <code>/common</code> tenant","text":"<p>The default Microsoft OAuth configuration uses the <code>common</code> tenant endpoint (<code>login.microsoftonline.com/common/...</code>), which supports both personal Microsoft accounts and Azure AD organizational accounts. If your organization requires a specific tenant, you can create a custom <code>AuthConfig</code> with the tenant-specific URLs.</p>"},{"location":"guide/batch/","title":"Batch Processing","text":""},{"location":"guide/batch/#basic-usage","title":"Basic usage","text":"<pre><code>planopticon batch -i ./recordings -o ./output --title \"Sprint Reviews\"\n</code></pre>"},{"location":"guide/batch/#how-it-works","title":"How it works","text":"<p>Batch mode:</p> <ol> <li>Scans the input directory for video files matching the pattern</li> <li>Processes each video through the full single-video pipeline</li> <li>Merges knowledge graphs across all videos with fuzzy matching and conflict resolution</li> <li>Generates a batch summary with aggregated stats and action items</li> <li>Writes a batch manifest linking to per-video results</li> </ol>"},{"location":"guide/batch/#file-patterns","title":"File patterns","text":"<pre><code># Default: common video formats\nplanopticon batch -i ./recordings -o ./output\n\n# Custom patterns\nplanopticon batch -i ./recordings -o ./output --pattern \"*.mp4,*.mov\"\n</code></pre>"},{"location":"guide/batch/#output-structure","title":"Output structure","text":"<pre><code>output/\n\u251c\u2500\u2500 batch_manifest.json # Batch-level manifest\n\u251c\u2500\u2500 batch_summary.md # Aggregated summary\n\u251c\u2500\u2500 knowledge_graph.db # Merged KG across all videos (SQLite, primary)\n\u251c\u2500\u2500 knowledge_graph.json # Merged KG across all videos (JSON export)\n\u2514\u2500\u2500 videos/\n \u251c\u2500\u2500 meeting-01/\n \u2502 \u251c\u2500\u2500 manifest.json\n \u2502 \u251c\u2500\u2500 transcript/\n \u2502 \u251c\u2500\u2500 diagrams/\n \u2502 \u251c\u2500\u2500 captures/\n \u2502 \u2514\u2500\u2500 results/\n \u2502 \u251c\u2500\u2500 analysis.md\n \u2502 \u251c\u2500\u2500 analysis.html\n \u2502 \u251c\u2500\u2500 knowledge_graph.db\n \u2502 \u251c\u2500\u2500 knowledge_graph.json\n \u2502 \u251c\u2500\u2500 key_points.json\n \u2502 \u2514\u2500\u2500 action_items.json\n \u2514\u2500\u2500 meeting-02/\n \u251c\u2500\u2500 manifest.json\n \u2514\u2500\u2500 ...\n</code></pre>"},{"location":"guide/batch/#knowledge-graph-merging","title":"Knowledge graph merging","text":"<p>When the same entity appears across multiple videos, PlanOpticon merges them using a multi-strategy approach:</p>"},{"location":"guide/batch/#entity-deduplication","title":"Entity deduplication","text":"<ul> <li>Case-insensitive exact matching -- <code>\"kubernetes\"</code> and <code>\"Kubernetes\"</code> are recognized as the same entity</li> <li>Fuzzy name matching -- Uses <code>SequenceMatcher</code> with a threshold of 0.85 to unify near-duplicate entities (e.g., <code>\"K8s\"</code> and <code>\"k8s cluster\"</code> may be matched depending on context)</li> <li>Descriptions are unioned -- All unique descriptions from each video are combined</li> <li>Occurrences are concatenated with source tracking -- Each occurrence retains its source video reference</li> </ul>"},{"location":"guide/batch/#relationship-deduplication","title":"Relationship deduplication","text":"<ul> <li>Relationships are deduplicated by (source, target, type) tuple</li> <li>Descriptions from duplicate relationships are merged</li> </ul>"},{"location":"guide/batch/#type-conflict-resolution","title":"Type conflict resolution","text":"<p>When the same entity appears with different types across videos, PlanOpticon uses a specificity ranking to resolve the conflict. More specific types are preferred over general ones:</p> <ul> <li><code>technology</code> > <code>concept</code></li> <li><code>person</code> > <code>concept</code></li> <li><code>organization</code> > <code>concept</code></li> <li>And so on through the full type hierarchy</li> </ul> <p>This ensures that an entity initially classified as a generic <code>concept</code> in one video gets upgraded to <code>technology</code> if it is identified more specifically in another.</p> <p>The merged knowledge graph is saved at the batch root in both SQLite (<code>knowledge_graph.db</code>) and JSON (<code>knowledge_graph.json</code>) formats, and is included in the batch summary as a Mermaid diagram.</p>"},{"location":"guide/batch/#error-handling","title":"Error handling","text":"<p>If a video fails to process, the batch continues. Failed videos are recorded in the batch manifest with error details:</p> <pre><code>{\n \"video_name\": \"corrupted-file\",\n \"status\": \"failed\",\n \"error\": \"Audio extraction failed: no audio track found\"\n}\n</code></pre> <p>The batch manifest tracks completion status:</p> <pre><code>{\n \"title\": \"Sprint Reviews\",\n \"total_videos\": 5,\n \"completed_videos\": 4,\n \"failed_videos\": 1,\n \"total_diagrams\": 12,\n \"total_action_items\": 23,\n \"total_key_points\": 45,\n \"videos\": [...],\n \"batch_summary_md\": \"batch_summary.md\",\n \"merged_knowledge_graph_json\": \"knowledge_graph.json\",\n \"merged_knowledge_graph_db\": \"knowledge_graph.db\"\n}\n</code></pre>"},{"location":"guide/batch/#using-batch-results","title":"Using batch results","text":""},{"location":"guide/batch/#query-the-merged-knowledge-graph","title":"Query the merged knowledge graph","text":"<p>After batch processing completes, the merged knowledge graph at the batch root contains entities and relationships from all successfully processed videos. You can query it just like a single-video knowledge graph:</p> <pre><code># Show stats for the merged graph\nplanopticon query --db output/knowledge_graph.db\n\n# List all people mentioned across all videos\nplanopticon query --db output/knowledge_graph.db \"entities --type person\"\n\n# See what connects to an entity across all videos\nplanopticon query --db output/knowledge_graph.db \"neighbors Alice\"\n\n# Ask natural language questions about the combined content\nplanopticon query --db output/knowledge_graph.db \"What technologies were discussed across all meetings?\"\n\n# Interactive REPL for exploration\nplanopticon query --db output/knowledge_graph.db -I\n</code></pre>"},{"location":"guide/batch/#export-merged-results","title":"Export merged results","text":"<p>All export commands work with the merged knowledge graph:</p> <pre><code># Generate documents from merged KG\nplanopticon export markdown output/knowledge_graph.db -o ./docs\n\n# Export as Obsidian vault\nplanopticon export obsidian output/knowledge_graph.db -o ./vault\n\n# Generate a project-wide exchange file\nplanopticon export exchange output/knowledge_graph.db --name \"Sprint Reviews Q4\"\n\n# Generate a GitHub wiki\nplanopticon wiki generate output/knowledge_graph.db -o ./wiki\n</code></pre>"},{"location":"guide/batch/#classify-for-planning","title":"Classify for planning","text":"<p>Run taxonomy classification on the merged graph to categorize entities across all videos:</p> <pre><code>planopticon kg classify output/knowledge_graph.db\n</code></pre>"},{"location":"guide/batch/#use-with-the-planning-agent","title":"Use with the planning agent","text":"<p>The planning agent can consume the merged knowledge graph for cross-video analysis and planning:</p> <pre><code>planopticon agent --db output/knowledge_graph.db\n</code></pre>"},{"location":"guide/batch/#incremental-batch-processing","title":"Incremental batch processing","text":"<p>If you add new videos to the recordings directory, you can re-run the batch command. Videos that have already been processed (with output directories present) will be detected via checkpoint/resume within each video's pipeline, making incremental processing efficient.</p> <pre><code># Add new recordings to the folder, then re-run\nplanopticon batch -i ./recordings -o ./output --title \"Sprint Reviews\"\n</code></pre>"},{"location":"guide/cloud-sources/","title":"Cloud Sources","text":"<p>PlanOpticon connects to 20+ source platforms for fetching videos, documents, and notes.</p>"},{"location":"guide/cloud-sources/#google-drive","title":"Google Drive","text":""},{"location":"guide/cloud-sources/#service-account-auth","title":"Service account auth","text":"<p>For automated/server-side usage:</p> <pre><code>export GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/service-account.json\"\nplanopticon batch --source gdrive --folder-id \"abc123\" -o ./output\n</code></pre>"},{"location":"guide/cloud-sources/#oauth2-user-auth","title":"OAuth2 user auth","text":"<p>For interactive usage with your own Google account:</p> <pre><code>planopticon auth google\nplanopticon batch --source gdrive --folder-id \"abc123\" -o ./output\n</code></pre>"},{"location":"guide/cloud-sources/#install","title":"Install","text":"<pre><code>pip install planopticon[gdrive]\n</code></pre>"},{"location":"guide/cloud-sources/#google-workspace-gws","title":"Google Workspace (gws)","text":"<p>Full Google Workspace integration beyond just Drive. Access Docs, Sheets, Slides, and Meet recordings through the <code>gws</code> CLI group.</p>"},{"location":"guide/cloud-sources/#setup","title":"Setup","text":"<pre><code>planopticon auth google\n</code></pre> <p>A single Google OAuth session covers Drive, Docs, Sheets, Slides, and Meet.</p>"},{"location":"guide/cloud-sources/#usage","title":"Usage","text":"<pre><code># List all Google Workspace content\nplanopticon gws list\n\n# List only Google Docs\nplanopticon gws list --type docs\n\n# List Meet recordings\nplanopticon gws list --type meet\n\n# Fetch a specific file\nplanopticon gws fetch abc123def --output ./downloads\n\n# Ingest an entire Drive folder into a knowledge base\nplanopticon gws ingest --folder-id abc123 --output ./kb --recursive\n</code></pre>"},{"location":"guide/cloud-sources/#microsoft-365-m365","title":"Microsoft 365 (m365)","text":"<p>Access OneDrive, SharePoint, Teams recordings, and Outlook content.</p>"},{"location":"guide/cloud-sources/#setup_1","title":"Setup","text":"<pre><code># Set your Azure AD app credentials\nexport MICROSOFT_CLIENT_ID=\"your-client-id\"\nexport MICROSOFT_CLIENT_SECRET=\"your-client-secret\"\n\n# Authenticate\nplanopticon auth microsoft\n</code></pre>"},{"location":"guide/cloud-sources/#usage_1","title":"Usage","text":"<pre><code># List all Microsoft 365 content\nplanopticon m365 list\n\n# List only Teams recordings\nplanopticon m365 list --type teams\n\n# List SharePoint files from a specific site\nplanopticon m365 list --type sharepoint --site \"Engineering\"\n\n# Fetch a specific file\nplanopticon m365 fetch item-id-123 --output ./downloads\n\n# Ingest SharePoint content into a knowledge base\nplanopticon m365 ingest --site \"Engineering\" --path \"/Shared Documents\" --output ./kb --recursive\n</code></pre>"},{"location":"guide/cloud-sources/#dropbox","title":"Dropbox","text":""},{"location":"guide/cloud-sources/#oauth2-auth","title":"OAuth2 auth","text":"<pre><code>planopticon auth dropbox\nplanopticon batch --source dropbox --folder \"/Recordings\" -o ./output\n</code></pre>"},{"location":"guide/cloud-sources/#install_1","title":"Install","text":"<pre><code>pip install planopticon[dropbox]\n</code></pre>"},{"location":"guide/cloud-sources/#youtube","title":"YouTube","text":"<p>PlanOpticon can ingest YouTube videos by URL. Audio is extracted and transcribed, and any visible content (slides, diagrams) is captured from frames.</p> <pre><code># Ingest a YouTube video\nplanopticon ingest \"https://www.youtube.com/watch?v=example\" --output ./kb\n\n# Ingest a playlist\nplanopticon ingest \"https://www.youtube.com/playlist?list=example\" --output ./kb\n</code></pre> <p>YouTube ingestion uses <code>yt-dlp</code> under the hood. Install it separately if not already available:</p> <pre><code>pip install yt-dlp\n</code></pre>"},{"location":"guide/cloud-sources/#meeting-recordings","title":"Meeting recordings","text":"<p>Access cloud recordings from Zoom, Microsoft Teams, and Google Meet.</p>"},{"location":"guide/cloud-sources/#zoom","title":"Zoom","text":"<pre><code># Set credentials and authenticate\nexport ZOOM_CLIENT_ID=\"your-client-id\"\nexport ZOOM_CLIENT_SECRET=\"your-client-secret\"\nplanopticon auth zoom\n\n# List recent Zoom recordings\nplanopticon recordings zoom-list\n\n# List recordings from a date range\nplanopticon recordings zoom-list --from 2026-01-01 --to 2026-02-01\n</code></pre>"},{"location":"guide/cloud-sources/#microsoft-teams","title":"Microsoft Teams","text":"<pre><code># Authenticate with Microsoft (covers Teams)\nplanopticon auth microsoft\n\n# List Teams recordings\nplanopticon recordings teams-list\n\n# List recordings from a date range\nplanopticon recordings teams-list --from 2026-01-01 --to 2026-02-01\n</code></pre>"},{"location":"guide/cloud-sources/#google-meet","title":"Google Meet","text":"<pre><code># Authenticate with Google (covers Meet)\nplanopticon auth google\n\n# List Meet recordings\nplanopticon recordings meet-list --limit 10\n</code></pre>"},{"location":"guide/cloud-sources/#notes-sources","title":"Notes sources","text":"<p>PlanOpticon can ingest notes and documents from several note-taking platforms.</p>"},{"location":"guide/cloud-sources/#obsidian","title":"Obsidian","text":"<p>Ingest an Obsidian vault directly. PlanOpticon follows wikilinks and parses frontmatter.</p> <pre><code>planopticon ingest ~/Obsidian/MyVault --output ./kb --recursive\n</code></pre>"},{"location":"guide/cloud-sources/#notion","title":"Notion","text":"<pre><code># Set your Notion integration token\nexport NOTION_API_KEY=\"secret_...\"\n\n# Authenticate\nplanopticon auth notion\n\n# Export knowledge base to Notion\nplanopticon export notion --input ./kb --parent-page abc123\n</code></pre>"},{"location":"guide/cloud-sources/#apple-notes","title":"Apple Notes","text":"<p>PlanOpticon can read Apple Notes on macOS via the system AppleScript bridge.</p> <pre><code>planopticon ingest --source apple-notes --output ./kb\n</code></pre>"},{"location":"guide/cloud-sources/#github","title":"GitHub","text":"<p>Ingest README files, wikis, and documentation from GitHub repositories.</p> <pre><code># Set your GitHub token\nexport GITHUB_TOKEN=\"ghp_...\"\n\n# Authenticate\nplanopticon auth github\n\n# Ingest a repo's docs\nplanopticon ingest \"github://ConflictHQ/PlanOpticon\" --output ./kb\n</code></pre>"},{"location":"guide/cloud-sources/#all-cloud-sources","title":"All cloud sources","text":"<pre><code>pip install planopticon[cloud]\n</code></pre>"},{"location":"guide/companion/","title":"Interactive Companion REPL","text":"<p>The PlanOpticon Companion is an interactive Read-Eval-Print Loop (REPL) that provides a conversational interface to PlanOpticon's full feature set. It combines workspace awareness, knowledge graph querying, LLM-powered chat, and planning agent skills into a single session.</p> <p>Use the Companion when you want to explore a knowledge graph interactively, ask natural-language questions about extracted content, generate planning artifacts on the fly, or switch between providers and models without restarting.</p>"},{"location":"guide/companion/#launching-the-companion","title":"Launching the Companion","text":"<p>There are three equivalent ways to start the Companion.</p>"},{"location":"guide/companion/#as-a-subcommand","title":"As a subcommand","text":"<pre><code>planopticon companion\n</code></pre>"},{"location":"guide/companion/#with-the-chat-c-flag","title":"With the <code>--chat</code> / <code>-C</code> flag","text":"<pre><code>planopticon --chat\nplanopticon -C\n</code></pre> <p>These flags launch the Companion directly from the top-level CLI, without invoking a subcommand.</p>"},{"location":"guide/companion/#with-options","title":"With options","text":"<p>The <code>companion</code> subcommand accepts options for specifying knowledge base paths, LLM provider, and model:</p> <pre><code># Point at a specific knowledge base\nplanopticon companion --kb ./results\n\n# Use a specific provider\nplanopticon companion -p anthropic\n\n# Use a specific model\nplanopticon companion --chat-model gpt-4o\n\n# Combine options\nplanopticon companion --kb ./results -p openai --chat-model gpt-4o\n</code></pre> Option Description <code>--kb PATH</code> Path to a knowledge graph file or directory (repeatable) <code>-p, --provider NAME</code> LLM provider: <code>auto</code>, <code>openai</code>, <code>anthropic</code>, <code>gemini</code>, <code>ollama</code>, <code>azure</code>, <code>together</code>, <code>fireworks</code>, <code>cerebras</code>, <code>xai</code> <code>--chat-model NAME</code> Override the default chat model for the selected provider"},{"location":"guide/companion/#auto-discovery","title":"Auto-discovery","text":"<p>On startup, the Companion automatically scans the workspace for relevant files:</p> <p>Knowledge graphs. The Companion uses <code>find_nearest_graph()</code> to locate the closest <code>knowledge_graph.db</code> or <code>knowledge_graph.json</code> file. It searches the current directory, common output subdirectories (<code>results/</code>, <code>output/</code>, <code>knowledge-base/</code>), recursively downward (up to 4 levels), and upward through parent directories. SQLite <code>.db</code> files are preferred over <code>.json</code> files.</p> <p>Videos. The current directory is scanned for files with <code>.mp4</code>, <code>.mkv</code>, and <code>.webm</code> extensions.</p> <p>Documents. The current directory is scanned for files with <code>.md</code>, <code>.pdf</code>, and <code>.docx</code> extensions.</p> <p>LLM provider. If <code>--provider</code> is set to <code>auto</code> (the default), the Companion attempts to initialise a provider using any available API key in the environment (<code>OPENAI_API_KEY</code>, <code>ANTHROPIC_API_KEY</code>, <code>GEMINI_API_KEY</code>, etc.).</p> <p>All discovered context is displayed in the welcome banner:</p> <pre><code> PlanOpticon Companion\n Interactive planning REPL\n\n Knowledge graph: knowledge_graph.db (42 entities, 87 relationships)\n Videos: meeting-2024-01-15.mp4, sprint-review.mp4\n Docs: requirements.md, architecture.pdf\n LLM provider: openai (model: gpt-4o)\n\n Type /help for commands, or ask a question.\n</code></pre> <p>If no knowledge graph is found, the banner shows \"No knowledge graph loaded.\" Commands that require a KG will return an appropriate message rather than failing silently.</p>"},{"location":"guide/companion/#slash-commands","title":"Slash Commands","text":"<p>The Companion supports 18 slash commands. Type <code>/help</code> at the prompt to see the full list.</p>"},{"location":"guide/companion/#help","title":"/help","text":"<p>Display all available commands with brief descriptions.</p> <pre><code>planopticon> /help\nAvailable commands:\n /help Show this help\n /status Workspace status\n /skills List available skills\n /entities [--type T] List KG entities\n /search TERM Search entities by name\n /neighbors ENTITY Show entity relationships\n /export FORMAT Export KG (markdown, obsidian, notion, csv)\n /analyze PATH Analyze a video/doc\n /ingest PATH Ingest a file into the KG\n /auth SERVICE Authenticate with a cloud service\n /provider [NAME] List or switch LLM provider\n /model [NAME] Show or switch chat model\n /run SKILL Run a skill by name\n /plan Run project_plan skill\n /prd Run PRD skill\n /tasks Run task_breakdown skill\n /quit, /exit Exit companion\n\nAny other input is sent to the chat agent (requires LLM).\n</code></pre>"},{"location":"guide/companion/#status","title":"/status","text":"<p>Show a summary of the current workspace state: loaded knowledge graph (with entity and relationship counts, broken down by entity type), number of discovered videos and documents, and whether an LLM provider is active.</p> <pre><code>planopticon> /status\nWorkspace status:\n KG: /home/user/project/results/knowledge_graph.db (42 entities, 87 relationships)\n technology: 15\n person: 12\n concept: 10\n organization: 5\n Videos: 2 found\n Docs: 3 found\n Provider: active\n</code></pre>"},{"location":"guide/companion/#skills","title":"/skills","text":"<p>List all registered planning agent skills with their names and descriptions. These are the skills that can be invoked via <code>/run</code>.</p> <pre><code>planopticon> /skills\nAvailable skills:\n project_plan: Generate a structured project plan from knowledge graph\n prd: Generate a product requirements document (PRD) / feature spec\n roadmap: Generate a product/project roadmap\n task_breakdown: Break down goals into tasks with dependencies\n github_issues: Generate GitHub issues from task breakdown\n requirements_chat: Interactive requirements gathering via guided questions\n doc_generator: Generate technical documentation, ADRs, or meeting notes\n artifact_export: Export artifacts in agent-ready formats\n cli_adapter: Push artifacts to external tools via their CLIs\n notes_export: Export knowledge graph as structured notes (Obsidian, Notion)\n wiki_generator: Generate a GitHub wiki from knowledge graph and artifacts\n</code></pre>"},{"location":"guide/companion/#entities-type-type","title":"/entities [--type TYPE]","text":"<p>List entities from the loaded knowledge graph. Optionally filter by entity type.</p> <pre><code>planopticon> /entities\nFound 42 entities\n [technology] Python -- General-purpose programming language\n [person] Alice -- Lead engineer on the project\n [concept] Microservices -- Architectural pattern discussed\n ...\n\nplanopticon> /entities --type person\nFound 12 entities\n [person] Alice -- Lead engineer on the project\n [person] Bob -- Product manager\n ...\n</code></pre> <p>Note</p> <p>This command requires a loaded knowledge graph. If none is loaded, it returns \"No knowledge graph loaded.\"</p>"},{"location":"guide/companion/#search-term","title":"/search TERM","text":"<p>Search entities by name substring (case-insensitive).</p> <pre><code>planopticon> /search python\nFound 3 entities\n [technology] Python -- General-purpose programming language\n [technology] Python Flask -- Web framework for Python\n [concept] Python packaging -- Discussion of pip and packaging tools\n</code></pre>"},{"location":"guide/companion/#neighbors-entity","title":"/neighbors ENTITY","text":"<p>Show all entities and relationships connected to a given entity. This performs a breadth-first traversal (depth 1) from the named entity.</p> <pre><code>planopticon> /neighbors Alice\nFound 4 entities and 5 relationships\n [person] Alice -- Lead engineer on the project\n [technology] Python -- General-purpose programming language\n [organization] Acme Corp -- Employer\n [concept] Authentication -- Auth system design\n Alice --[works_with]--> Python\n Alice --[employed_by]--> Acme Corp\n Alice --[proposed]--> Authentication\n Bob --[collaborates_with]--> Alice\n Authentication --[discussed_by]--> Alice\n</code></pre>"},{"location":"guide/companion/#export-format","title":"/export FORMAT","text":"<p>Request an export of the knowledge graph. Supported formats: <code>markdown</code>, <code>obsidian</code>, <code>notion</code>, <code>csv</code>. This command prints the equivalent CLI command to run.</p> <pre><code>planopticon> /export obsidian\nExport 'obsidian' requested. Use the CLI command:\n planopticon export obsidian /home/user/project/results/knowledge_graph.db\n</code></pre>"},{"location":"guide/companion/#analyze-path","title":"/analyze PATH","text":"<p>Request analysis of a video or document file. Validates the file exists and prints the equivalent CLI command.</p> <pre><code>planopticon> /analyze meeting.mp4\nAnalyze requested for meeting.mp4. Use the CLI:\n planopticon analyze -i /home/user/project/meeting.mp4\n</code></pre>"},{"location":"guide/companion/#ingest-path","title":"/ingest PATH","text":"<p>Request ingestion of a file into the knowledge graph. Validates the file exists and prints the equivalent CLI command.</p> <pre><code>planopticon> /ingest notes.md\nIngest requested for notes.md. Use the CLI:\n planopticon ingest /home/user/project/notes.md\n</code></pre>"},{"location":"guide/companion/#auth-service","title":"/auth [SERVICE]","text":"<p>Authenticate with a cloud service. When called without arguments, lists all available services. When called with a service name, triggers the authentication flow.</p> <pre><code>planopticon> /auth\nUsage: /auth SERVICE\nAvailable: dropbox, github, google, microsoft, notion, zoom\n\nplanopticon> /auth zoom\nZoom authenticated (oauth)\n</code></pre>"},{"location":"guide/companion/#provider-name","title":"/provider [NAME]","text":"<p>List available LLM providers and their status, or switch to a different provider.</p> <p>When called without arguments (or with <code>list</code>), shows all known providers with their availability status:</p> <ul> <li>ready -- API key found in environment</li> <li>local -- runs locally (Ollama)</li> <li>no key -- no API key configured</li> </ul> <p>The currently active provider is marked.</p> <pre><code>planopticon> /provider\nAvailable providers:\n openai: ready (active)\n anthropic: ready\n gemini: no key\n ollama: local\n azure: no key\n together: no key\n fireworks: no key\n cerebras: no key\n xai: no key\n\nCurrent: openai\n</code></pre> <p>To switch providers at runtime:</p> <pre><code>planopticon> /provider anthropic\nSwitched to provider: anthropic\n</code></pre> <p>Switching the provider reinitialises the provider manager and the planning agent. The chat model is reset to the provider's default. If initialisation fails, an error message is shown.</p>"},{"location":"guide/companion/#model-name","title":"/model [NAME]","text":"<p>Show the current chat model, or switch to a different one.</p> <pre><code>planopticon> /model\nCurrent model: default\nUsage: /model MODEL_NAME\n\nplanopticon> /model claude-sonnet-4-20250514\nSwitched to model: claude-sonnet-4-20250514\n</code></pre> <p>Switching the model reinitialises both the provider manager and the planning agent.</p>"},{"location":"guide/companion/#run-skill","title":"/run SKILL","text":"<p>Run any registered skill by name. The skill receives the current agent context (knowledge graph, query engine, provider, and any previously generated artifacts) and returns an artifact.</p> <pre><code>planopticon> /run roadmap\n--- Roadmap (roadmap) ---\n# Roadmap\n\n## Vision & Strategy\n...\n</code></pre> <p>If the skill cannot execute (missing KG or provider), an error message is returned. Use <code>/skills</code> to see all available skill names.</p>"},{"location":"guide/companion/#plan","title":"/plan","text":"<p>Shortcut for <code>/run project_plan</code>. Generates a structured project plan from the loaded knowledge graph.</p> <pre><code>planopticon> /plan\n--- Project Plan (project_plan) ---\n# Project Plan\n\n## Executive Summary\n...\n</code></pre>"},{"location":"guide/companion/#prd","title":"/prd","text":"<p>Shortcut for <code>/run prd</code>. Generates a product requirements document.</p> <pre><code>planopticon> /prd\n--- Product Requirements Document (prd) ---\n# Product Requirements Document\n\n## Problem Statement\n...\n</code></pre>"},{"location":"guide/companion/#tasks","title":"/tasks","text":"<p>Shortcut for <code>/run task_breakdown</code>. Breaks goals and features into tasks with dependencies, priorities, and effort estimates. The output is JSON.</p> <pre><code>planopticon> /tasks\n--- Task Breakdown (task_list) ---\n[\n {\n \"id\": \"T1\",\n \"title\": \"Set up authentication service\",\n \"description\": \"Implement OAuth2 flow with JWT tokens\",\n \"depends_on\": [],\n \"priority\": \"high\",\n \"estimate\": \"1w\",\n \"assignee_role\": \"backend engineer\"\n },\n ...\n]\n</code></pre>"},{"location":"guide/companion/#quit-and-exit","title":"/quit and /exit","text":"<p>Exit the Companion REPL.</p> <pre><code>planopticon> /quit\nBye.\n</code></pre>"},{"location":"guide/companion/#exiting-the-companion","title":"Exiting the Companion","text":"<p>In addition to <code>/quit</code> and <code>/exit</code>, you can exit by:</p> <ul> <li>Typing <code>quit</code>, <code>exit</code>, <code>bye</code>, or <code>q</code> as bare words (without the <code>/</code> prefix)</li> <li>Pressing <code>Ctrl+C</code> or <code>Ctrl+D</code></li> </ul> <p>All of these end the session with a \"Bye.\" message.</p>"},{"location":"guide/companion/#chat-mode","title":"Chat Mode","text":"<p>Any input that does not start with <code>/</code> and is not a bare exit word is sent to the chat agent as a natural-language message. This requires a configured LLM provider.</p> <pre><code>planopticon> What technologies were discussed in the meeting?\nBased on the knowledge graph, the following technologies were discussed:\n\n1. **Python** -- mentioned in the context of backend development\n2. **React** -- proposed for the frontend redesign\n3. **PostgreSQL** -- discussed as the primary database\n...\n</code></pre> <p>The chat agent maintains conversation history across the session. It has full awareness of:</p> <ul> <li>The loaded knowledge graph (entity and relationship counts, types)</li> <li>Any artifacts generated during the session (via <code>/plan</code>, <code>/prd</code>, <code>/tasks</code>, <code>/run</code>)</li> <li>All available slash commands (which it may suggest when relevant)</li> <li>The full PlanOpticon CLI command set</li> </ul> <p>If no LLM provider is configured, chat mode returns an error with instructions:</p> <pre><code>planopticon> What was discussed?\nChat requires an LLM provider. Set one of:\n OPENAI_API_KEY\n ANTHROPIC_API_KEY\n GEMINI_API_KEY\nOr pass --provider / --chat-model.\n</code></pre>"},{"location":"guide/companion/#runtime-provider-and-model-switching","title":"Runtime Provider and Model Switching","text":"<p>One of the Companion's key features is the ability to switch LLM providers and models without restarting the session. This is useful for:</p> <ul> <li>Comparing outputs across different models</li> <li>Falling back to a local model (Ollama) when API keys expire</li> <li>Using a cheaper model for exploratory queries and a more capable one for artifact generation</li> </ul> <p>When you switch providers or models via <code>/provider</code> or <code>/model</code>, the Companion:</p> <ol> <li>Updates the internal provider name and/or model name</li> <li>Reinitialises the <code>ProviderManager</code></li> <li>Reinitialises the <code>PlanningAgent</code> with a fresh <code>AgentContext</code> that retains the loaded knowledge graph and query engine</li> </ol> <p>Conversation history is preserved across provider switches.</p>"},{"location":"guide/companion/#example-session","title":"Example Session","text":"<p>The following walkthrough shows a typical Companion session, from launch through exploration to artifact generation.</p> <pre><code>$ planopticon companion --kb ./results\n</code></pre> <pre><code> PlanOpticon Companion\n Interactive planning REPL\n\n Knowledge graph: knowledge_graph.db (58 entities, 124 relationships)\n Videos: sprint-review-2024-03.mp4\n Docs: architecture.md, requirements.pdf\n LLM provider: openai (model: default)\n\n Type /help for commands, or ask a question.\n\nplanopticon> /status\nWorkspace status:\n KG: /home/user/project/results/knowledge_graph.db (58 entities, 124 relationships)\n technology: 20\n person: 15\n concept: 13\n organization: 8\n time: 2\n Videos: 1 found\n Docs: 2 found\n Provider: active\n\nplanopticon> /entities --type person\nFound 15 entities\n [person] Alice -- Lead architect\n [person] Bob -- Product manager\n [person] Carol -- Frontend lead\n ...\n\nplanopticon> /neighbors Alice\nFound 6 entities and 8 relationships\n [person] Alice -- Lead architect\n [technology] Kubernetes -- Container orchestration platform\n [concept] Microservices -- Proposed architecture pattern\n ...\n Alice --[proposed]--> Microservices\n Alice --[expert_in]--> Kubernetes\n ...\n\nplanopticon> What were the main decisions made in the sprint review?\nBased on the knowledge graph, the sprint review covered several key decisions:\n\n1. **Adopt microservices architecture** -- Alice proposed and the team agreed\n to move from the monolith to a microservices pattern.\n2. **Use Kubernetes for orchestration** -- Selected over Docker Swarm.\n3. **Prioritize authentication module** -- Bob identified this as the highest\n priority for the next sprint.\n\nplanopticon> /provider anthropic\nSwitched to provider: anthropic\n\nplanopticon> /model claude-sonnet-4-20250514\nSwitched to model: claude-sonnet-4-20250514\n\nplanopticon> /plan\n--- Project Plan (project_plan) ---\n# Project Plan\n\n## Executive Summary\nThis project plan outlines the migration from a monolithic architecture\nto a microservices-based system, as discussed in the sprint review...\n\n## Goals & Objectives\n...\n\nplanopticon> /tasks\n--- Task Breakdown (task_list) ---\n[\n {\n \"id\": \"T1\",\n \"title\": \"Design service boundaries\",\n \"description\": \"Define microservice boundaries based on domain analysis\",\n \"depends_on\": [],\n \"priority\": \"high\",\n \"estimate\": \"3d\",\n \"assignee_role\": \"architect\"\n },\n ...\n]\n\nplanopticon> /export obsidian\nExport 'obsidian' requested. Use the CLI command:\n planopticon export obsidian /home/user/project/results/knowledge_graph.db\n\nplanopticon> quit\nBye.\n</code></pre>"},{"location":"guide/document-ingestion/","title":"Document Ingestion","text":"<p>Document ingestion lets you process files -- PDFs, Markdown, and plaintext -- into a knowledge graph. PlanOpticon extracts text from documents, chunks it into manageable pieces, runs LLM-powered entity and relationship extraction, and stores the results in a FalkorDB knowledge graph. This is the same knowledge graph format produced by video analysis, so you can combine video and document insights in a single graph.</p>"},{"location":"guide/document-ingestion/#supported-formats","title":"Supported formats","text":"Extension Processor Description <code>.pdf</code> <code>PdfProcessor</code> Extracts text page by page using pymupdf or pdfplumber <code>.md</code>, <code>.markdown</code> <code>MarkdownProcessor</code> Splits on headings into sections <code>.txt</code>, <code>.text</code>, <code>.log</code>, <code>.csv</code> <code>PlaintextProcessor</code> Splits on paragraph boundaries <p>Additional formats can be added by implementing the <code>DocumentProcessor</code> base class and registering it (see Extending with custom processors below).</p>"},{"location":"guide/document-ingestion/#cli-usage","title":"CLI usage","text":""},{"location":"guide/document-ingestion/#planopticon-ingest","title":"<code>planopticon ingest</code>","text":"<pre><code>planopticon ingest INPUT_PATH [OPTIONS]\n</code></pre> <p>Arguments:</p> Argument Description <code>INPUT_PATH</code> Path to a file or directory to ingest (must exist) <p>Options:</p> Option Short Default Description <code>--output</code> <code>-o</code> Current directory Output directory for the knowledge graph <code>--db-path</code> None Path to an existing <code>knowledge_graph.db</code> to merge into <code>--recursive / --no-recursive</code> <code>-r</code> <code>--recursive</code> Recurse into subdirectories (directory ingestion only) <code>--provider</code> <code>-p</code> <code>auto</code> LLM provider for entity extraction (<code>openai</code>, <code>anthropic</code>, <code>gemini</code>, <code>ollama</code>, <code>azure</code>, <code>together</code>, <code>fireworks</code>, <code>cerebras</code>, <code>xai</code>) <code>--chat-model</code> None Override the model used for LLM entity extraction"},{"location":"guide/document-ingestion/#single-file-ingestion","title":"Single file ingestion","text":"<p>Process a single document and create a new knowledge graph:</p> <pre><code>planopticon ingest spec.md\n</code></pre> <p>This creates <code>knowledge_graph.db</code> and <code>knowledge_graph.json</code> in the current directory.</p> <p>Specify an output directory:</p> <pre><code>planopticon ingest report.pdf -o ./results\n</code></pre> <p>This creates <code>./results/knowledge_graph.db</code> and <code>./results/knowledge_graph.json</code>.</p>"},{"location":"guide/document-ingestion/#directory-ingestion","title":"Directory ingestion","text":"<p>Process all supported files in a directory:</p> <pre><code>planopticon ingest ./docs/\n</code></pre> <p>By default, this recurses into subdirectories. To process only the top-level directory:</p> <pre><code>planopticon ingest ./docs/ --no-recursive\n</code></pre> <p>PlanOpticon automatically filters for supported file extensions. Unsupported files are silently skipped.</p>"},{"location":"guide/document-ingestion/#merging-into-an-existing-knowledge-graph","title":"Merging into an existing knowledge graph","text":"<p>To add document content to an existing knowledge graph (e.g., one created from video analysis), use <code>--db-path</code>:</p> <pre><code># First, analyze a video\nplanopticon analyze meeting.mp4 -o ./results\n\n# Then, ingest supplementary documents into the same graph\nplanopticon ingest ./meeting-notes/ --db-path ./results/knowledge_graph.db\n</code></pre> <p>The ingested entities and relationships are merged with the existing graph. Duplicate entities are consolidated automatically by the knowledge graph engine.</p>"},{"location":"guide/document-ingestion/#choosing-an-llm-provider","title":"Choosing an LLM provider","text":"<p>Entity and relationship extraction requires an LLM. By default, PlanOpticon auto-detects available providers based on your environment variables. You can override this:</p> <pre><code># Use Anthropic for extraction\nplanopticon ingest docs/ -p anthropic\n\n# Use a specific model\nplanopticon ingest docs/ -p openai --chat-model gpt-4o\n\n# Use a local Ollama model\nplanopticon ingest docs/ -p ollama --chat-model llama3\n</code></pre>"},{"location":"guide/document-ingestion/#output","title":"Output","text":"<p>After ingestion, PlanOpticon prints a summary:</p> <pre><code>Knowledge graph: ./knowledge_graph.db\n spec.md: 12 chunks\n architecture.md: 8 chunks\n requirements.txt: 3 chunks\n\nIngestion complete:\n Files processed: 3\n Total chunks: 23\n Entities extracted: 47\n Relationships: 31\n Knowledge graph: ./knowledge_graph.db\n</code></pre> <p>Both <code>.db</code> (SQLite/FalkorDB) and <code>.json</code> formats are saved automatically.</p>"},{"location":"guide/document-ingestion/#how-each-processor-works","title":"How each processor works","text":""},{"location":"guide/document-ingestion/#pdf-processor","title":"PDF processor","text":"<p>The <code>PdfProcessor</code> extracts text from PDF files on a per-page basis. It tries two extraction libraries in order:</p> <ol> <li>pymupdf (preferred) -- Fast, reliable text extraction. Install with <code>pip install pymupdf</code>.</li> <li>pdfplumber (fallback) -- Alternative extractor. Install with <code>pip install pdfplumber</code>.</li> </ol> <p>If neither library is installed, the processor raises an <code>ImportError</code> with installation instructions.</p> <p>Each page becomes a separate <code>DocumentChunk</code> with:</p> <ul> <li><code>text</code>: The extracted text content of the page</li> <li><code>page</code>: The 1-based page number</li> <li><code>metadata.extraction_method</code>: Which library was used (<code>pymupdf</code> or <code>pdfplumber</code>)</li> </ul> <p>To install PDF support:</p> <pre><code>pip install 'planopticon[pdf]'\n# or\npip install pymupdf\n# or\npip install pdfplumber\n</code></pre>"},{"location":"guide/document-ingestion/#markdown-processor","title":"Markdown processor","text":"<p>The <code>MarkdownProcessor</code> splits Markdown files on heading boundaries (lines starting with <code>#</code> through <code>######</code>). Each heading and its content until the next heading becomes a separate chunk.</p> <p>Splitting behavior:</p> <ul> <li>If the file contains headings, each heading section becomes a chunk. The <code>section</code> field records the heading text.</li> <li>Content before the first heading is captured as a <code>(preamble)</code> chunk.</li> <li>If the file contains no headings, it falls back to paragraph-based chunking (same as plaintext).</li> </ul> <p>For example, a file with this structure:</p> <pre><code>Some intro text.\n\n# Architecture\n\nThe system uses a microservices architecture...\n\n## Components\n\nThere are three main components...\n\n# Deployment\n\nDeployment is handled via...\n</code></pre> <p>Produces four chunks: <code>(preamble)</code>, <code>Architecture</code>, <code>Components</code>, and <code>Deployment</code>.</p>"},{"location":"guide/document-ingestion/#plaintext-processor","title":"Plaintext processor","text":"<p>The <code>PlaintextProcessor</code> handles <code>.txt</code>, <code>.text</code>, <code>.log</code>, and <code>.csv</code> files. It splits text on paragraph boundaries (double newlines) and groups paragraphs into chunks with a configurable maximum size.</p> <p>Chunking parameters:</p> Parameter Default Description <code>max_chunk_size</code> 2000 characters Maximum size of each chunk <code>overlap</code> 200 characters Number of characters from the end of one chunk to repeat at the start of the next <p>The overlap ensures that entities or context that spans a paragraph boundary are not lost. Chunks are created by accumulating paragraphs until the next paragraph would exceed <code>max_chunk_size</code>, at which point the current chunk is flushed and a new one begins.</p>"},{"location":"guide/document-ingestion/#the-ingestion-pipeline","title":"The ingestion pipeline","text":"<p>Document ingestion follows this pipeline:</p> <pre><code>File on disk\n |\n v\nProcessor selection (by file extension)\n |\n v\nText extraction (PDF pages / Markdown sections / plaintext paragraphs)\n |\n v\nDocumentChunk objects (text + metadata)\n |\n v\nSource registration (provenance tracking in the KG)\n |\n v\nKG content addition (LLM entity/relationship extraction per chunk)\n |\n v\nKnowledge graph storage (.db + .json)\n</code></pre>"},{"location":"guide/document-ingestion/#step-1-processor-selection","title":"Step 1: Processor selection","text":"<p>PlanOpticon maintains a registry of processors keyed by file extension. When you call <code>ingest_file()</code>, it looks up the appropriate processor using <code>get_processor(path)</code>. If no processor is registered for the file extension, a <code>ValueError</code> is raised.</p>"},{"location":"guide/document-ingestion/#step-2-text-extraction","title":"Step 2: Text extraction","text":"<p>The selected processor reads the file and produces a list of <code>DocumentChunk</code> objects. Each chunk contains:</p> Field Type Description <code>text</code> <code>str</code> The extracted text content <code>source_file</code> <code>str</code> Path to the source file <code>chunk_index</code> <code>int</code> Sequential index of this chunk within the file <code>page</code> <code>Optional[int]</code> Page number (PDF only, 1-based) <code>section</code> <code>Optional[str]</code> Section heading (Markdown only) <code>metadata</code> <code>Dict[str, Any]</code> Additional metadata (e.g., extraction method)"},{"location":"guide/document-ingestion/#step-3-source-registration","title":"Step 3: Source registration","text":"<p>Each ingested file is registered as a source in the knowledge graph with provenance metadata:</p> <ul> <li><code>source_id</code>: A SHA-256 hash of the absolute file path (first 12 characters), unless you provide a custom ID</li> <li><code>source_type</code>: Always <code>\"document\"</code></li> <li><code>title</code>: The file stem (filename without extension)</li> <li><code>path</code>: The file path</li> <li><code>mime_type</code>: Detected MIME type</li> <li><code>ingested_at</code>: ISO-8601 timestamp</li> <li><code>metadata</code>: Chunk count and file extension</li> </ul>"},{"location":"guide/document-ingestion/#step-4-entity-and-relationship-extraction","title":"Step 4: Entity and relationship extraction","text":"<p>Each chunk's text is passed to <code>knowledge_graph.add_content()</code>, which uses the configured LLM provider to extract entities and relationships. The content source is tagged with the document name and either the page number or section name:</p> <ul> <li><code>document:report.pdf:page:3</code></li> <li><code>document:spec.md:section:Architecture</code></li> <li><code>document:notes.txt</code> (no page or section)</li> </ul>"},{"location":"guide/document-ingestion/#step-5-storage","title":"Step 5: Storage","text":"<p>The knowledge graph is saved in both <code>.db</code> (SQLite-backed FalkorDB) and <code>.json</code> formats.</p>"},{"location":"guide/document-ingestion/#combining-with-video-analysis","title":"Combining with video analysis","text":"<p>A common workflow is to analyze a video recording and then ingest related documents into the same knowledge graph:</p> <pre><code># Step 1: Analyze the meeting recording\nplanopticon analyze meeting-recording.mp4 -o ./project-kg\n\n# Step 2: Ingest the meeting agenda\nplanopticon ingest agenda.md --db-path ./project-kg/knowledge_graph.db\n\n# Step 3: Ingest the project spec\nplanopticon ingest project-spec.pdf --db-path ./project-kg/knowledge_graph.db\n\n# Step 4: Ingest a whole docs folder\nplanopticon ingest ./reference-docs/ --db-path ./project-kg/knowledge_graph.db\n\n# Step 5: Query the combined graph\nplanopticon query --db-path ./project-kg/knowledge_graph.db\n</code></pre> <p>The resulting knowledge graph contains entities and relationships from all sources -- video transcripts, meeting agendas, specs, and reference documents -- with full provenance tracking so you can trace any entity back to its source.</p>"},{"location":"guide/document-ingestion/#python-api","title":"Python API","text":""},{"location":"guide/document-ingestion/#ingesting-a-single-file","title":"Ingesting a single file","text":"<pre><code>from pathlib import Path\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\nfrom video_processor.processors.ingest import ingest_file\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nchunk_count = ingest_file(Path(\"document.pdf\"), kg)\nprint(f\"Processed {chunk_count} chunks\")\n\nkg.save(Path(\"knowledge_graph.db\"))\n</code></pre>"},{"location":"guide/document-ingestion/#ingesting-a-directory","title":"Ingesting a directory","text":"<pre><code>from pathlib import Path\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\nfrom video_processor.processors.ingest import ingest_directory\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nresults = ingest_directory(\n Path(\"./docs\"),\n kg,\n recursive=True,\n extensions=[\".md\", \".pdf\"], # Optional: filter by extension\n)\n\nfor filepath, chunks in results.items():\n print(f\" {filepath}: {chunks} chunks\")\n\nkg.save(Path(\"knowledge_graph.db\"))\n</code></pre>"},{"location":"guide/document-ingestion/#listing-supported-extensions","title":"Listing supported extensions","text":"<pre><code>from video_processor.processors.base import list_supported_extensions\n\nextensions = list_supported_extensions()\nprint(extensions)\n# ['.csv', '.log', '.markdown', '.md', '.pdf', '.text', '.txt']\n</code></pre>"},{"location":"guide/document-ingestion/#working-with-processors-directly","title":"Working with processors directly","text":"<pre><code>from pathlib import Path\nfrom video_processor.processors.base import get_processor\n\nprocessor = get_processor(Path(\"report.pdf\"))\nif processor:\n chunks = processor.process(Path(\"report.pdf\"))\n for chunk in chunks:\n print(f\"Page {chunk.page}: {chunk.text[:100]}...\")\n</code></pre>"},{"location":"guide/document-ingestion/#extending-with-custom-processors","title":"Extending with custom processors","text":"<p>To add support for a new file format, implement the <code>DocumentProcessor</code> abstract class and register it:</p> <pre><code>from pathlib import Path\nfrom typing import List\nfrom video_processor.processors.base import (\n DocumentChunk,\n DocumentProcessor,\n register_processor,\n)\n\n\nclass HtmlProcessor(DocumentProcessor):\n supported_extensions = [\".html\", \".htm\"]\n\n def can_process(self, path: Path) -> bool:\n return path.suffix.lower() in self.supported_extensions\n\n def process(self, path: Path) -> List[DocumentChunk]:\n from bs4 import BeautifulSoup\n\n soup = BeautifulSoup(path.read_text(), \"html.parser\")\n text = soup.get_text(separator=\"\\n\")\n return [\n DocumentChunk(\n text=text,\n source_file=str(path),\n chunk_index=0,\n )\n ]\n\n\nregister_processor(HtmlProcessor.supported_extensions, HtmlProcessor)\n</code></pre> <p>After registration, <code>planopticon ingest</code> will automatically handle <code>.html</code> and <code>.htm</code> files.</p>"},{"location":"guide/document-ingestion/#companion-repl","title":"Companion REPL","text":"<p>Inside the interactive companion REPL, you can ingest files using the <code>/ingest</code> command:</p> <pre><code>> /ingest ./meeting-notes.md\nIngested meeting-notes.md: 5 chunks\n</code></pre> <p>This adds content to the currently loaded knowledge graph.</p>"},{"location":"guide/document-ingestion/#common-workflows","title":"Common workflows","text":""},{"location":"guide/document-ingestion/#build-a-project-knowledge-base-from-scratch","title":"Build a project knowledge base from scratch","text":"<pre><code># Ingest all project docs\nplanopticon ingest ./project-docs/ -o ./knowledge-base\n\n# Query what was captured\nplanopticon query --db-path ./knowledge-base/knowledge_graph.db\n\n# Export as an Obsidian vault\nplanopticon export obsidian ./knowledge-base/knowledge_graph.db -o ./vault\n</code></pre>"},{"location":"guide/document-ingestion/#incrementally-build-a-knowledge-graph","title":"Incrementally build a knowledge graph","text":"<pre><code># Start with initial docs\nplanopticon ingest ./sprint-1-docs/ -o ./kg\n\n# Add more docs over time\nplanopticon ingest ./sprint-2-docs/ --db-path ./kg/knowledge_graph.db\nplanopticon ingest ./sprint-3-docs/ --db-path ./kg/knowledge_graph.db\n\n# The graph grows with each ingestion\nplanopticon query --db-path ./kg/knowledge_graph.db stats\n</code></pre>"},{"location":"guide/document-ingestion/#ingest-from-google-workspace-or-microsoft-365","title":"Ingest from Google Workspace or Microsoft 365","text":"<p>PlanOpticon provides integrated commands that fetch cloud documents and ingest them in one step:</p> <pre><code># Google Workspace\nplanopticon gws ingest --folder-id FOLDER_ID -o ./results\n\n# Microsoft 365 / SharePoint\nplanopticon m365 ingest --web-url https://contoso.sharepoint.com/sites/proj \\\n --folder-url /sites/proj/Shared\\ Documents\n</code></pre> <p>These commands handle authentication, document download, text extraction, and knowledge graph creation automatically.</p>"},{"location":"guide/export/","title":"Export","text":"<p>PlanOpticon provides multiple ways to export knowledge graph data into formats suitable for documentation, note-taking, collaboration, and interchange. All export commands work offline from a <code>knowledge_graph.db</code> file -- no API key is needed for template-based exports.</p>"},{"location":"guide/export/#overview-of-export-options","title":"Overview of export options","text":"Format Command API Key Description Markdown documents <code>planopticon export markdown</code> No 7 document types: summary, meeting notes, glossary, and more Obsidian vault <code>planopticon export obsidian</code> No YAML frontmatter, <code>[[wiki-links]]</code>, tag pages, Map of Content Notion-compatible <code>planopticon export notion</code> No Callout blocks, CSV database for bulk import PlanOpticonExchange JSON <code>planopticon export exchange</code> No Canonical interchange format for merging and sharing GitHub wiki <code>planopticon wiki generate</code> No Home, Sidebar, entity pages, type indexes GitHub wiki push <code>planopticon wiki push</code> Git auth Push generated wiki to a GitHub repo"},{"location":"guide/export/#markdown-document-generator","title":"Markdown document generator","text":"<p>The markdown exporter produces structured documents from knowledge graph data using pure template-based generation. No LLM calls are made -- the output is deterministic and based entirely on the entities and relationships in the graph.</p>"},{"location":"guide/export/#cli-usage","title":"CLI usage","text":"<pre><code>planopticon export markdown DB_PATH [OPTIONS]\n</code></pre> <p>Arguments:</p> Argument Description <code>DB_PATH</code> Path to a <code>knowledge_graph.db</code> file <p>Options:</p> Option Short Default Description <code>--output</code> <code>-o</code> <code>./export</code> Output directory <code>--type</code> <code>all</code> Document types to generate (repeatable). Choices: <code>summary</code>, <code>meeting-notes</code>, <code>glossary</code>, <code>relationship-map</code>, <code>status-report</code>, <code>entity-index</code>, <code>csv</code>, <code>all</code> <p>Examples:</p> <pre><code># Generate all document types\nplanopticon export markdown knowledge_graph.db\n\n# Generate only summary and glossary\nplanopticon export markdown kg.db -o ./docs --type summary --type glossary\n\n# Generate meeting notes and CSV\nplanopticon export markdown kg.db --type meeting-notes --type csv\n</code></pre>"},{"location":"guide/export/#document-types","title":"Document types","text":""},{"location":"guide/export/#summary-executive-summary","title":"summary (Executive Summary)","text":"<p>A high-level overview of the knowledge graph. Contains:</p> <ul> <li>Total entity and relationship counts</li> <li>Entity breakdown by type (table with counts and example names)</li> <li>Key entities ranked by number of connections (top 10)</li> <li>Relationship type breakdown with counts</li> </ul> <p>This is useful for getting a quick overview of what a knowledge base contains.</p>"},{"location":"guide/export/#meeting-notes-meeting-notes","title":"meeting-notes (Meeting Notes)","text":"<p>Formats knowledge graph data as structured meeting notes. Organizes entities into planning-relevant categories:</p> <ul> <li>Discussion Topics: Entities of type <code>concept</code>, <code>technology</code>, or <code>topic</code> with their descriptions</li> <li>Participants: Entities of type <code>person</code></li> <li>Decisions & Constraints: Entities of type <code>decision</code> or <code>constraint</code></li> <li>Action Items: Entities of type <code>goal</code>, <code>feature</code>, or <code>milestone</code>, shown as checkboxes. If an entity has an <code>assigned_to</code> or <code>owned_by</code> relationship, the owner is shown as <code>@name</code></li> <li>Open Questions / Loose Ends: Entities with one or fewer relationships (excluding people), indicating topics that may need follow-up</li> </ul> <p>Includes a generation timestamp.</p>"},{"location":"guide/export/#glossary-glossary","title":"glossary (Glossary)","text":"<p>An alphabetically sorted dictionary of all entities in the knowledge graph. Each entry shows:</p> <ul> <li>Entity name (bold)</li> <li>Entity type (italic, in parentheses)</li> <li>First description</li> </ul> <p>Format:</p> <pre><code>**Entity Name** *(type)*\n: Description text here.\n</code></pre>"},{"location":"guide/export/#relationship-map-relationship-map","title":"relationship-map (Relationship Map)","text":"<p>A comprehensive view of all relationships in the graph, organized by relationship type. Each type gets its own section with a table of source-target pairs.</p> <p>Also includes a Mermaid diagram of the top 20 most-connected entities, rendered as a <code>graph LR</code> flowchart with labeled edges. This diagram can be rendered natively in GitHub, GitLab, Obsidian, and many other Markdown viewers.</p>"},{"location":"guide/export/#status-report-status-report","title":"status-report (Status Report)","text":"<p>A project-oriented status report that highlights planning entities:</p> <ul> <li>Overview: Counts of entities, relationships, features, milestones, requirements, and risks/constraints</li> <li>Milestones: Entities of type <code>milestone</code> with descriptions</li> <li>Features: Table of entities of type <code>feature</code> with descriptions (truncated to 60 characters)</li> <li>Risks & Constraints: Entities of type <code>risk</code> or <code>constraint</code></li> </ul> <p>Includes a generation timestamp.</p>"},{"location":"guide/export/#entity-index-entity-index","title":"entity-index (Entity Index)","text":"<p>A master index of all entities grouped by type. Each type section lists entities alphabetically with their first description. Shows total entity count and number of types.</p>"},{"location":"guide/export/#csv-csv-export","title":"csv (CSV Export)","text":"<p>A CSV file suitable for spreadsheet import. Columns:</p> Column Description Name Entity name Type Entity type Description First description Related To Semicolon-separated list of entities this entity has outgoing relationships to Source First occurrence source"},{"location":"guide/export/#entity-briefs","title":"Entity briefs","text":"<p>In addition to the selected document types, the <code>generate_all()</code> function automatically creates individual entity brief pages in an <code>entities/</code> subdirectory. Each brief contains:</p> <ul> <li>Entity name and type</li> <li>Summary (all descriptions)</li> <li>Outgoing relationships (table of target entities and relationship types)</li> <li>Incoming relationships (table of source entities and relationship types)</li> <li>Source occurrences with timestamps and context text</li> </ul>"},{"location":"guide/export/#obsidian-vault-export","title":"Obsidian vault export","text":"<p>The Obsidian exporter creates a complete vault structure with YAML frontmatter, <code>[[wiki-links]]</code> for entity cross-references, and Obsidian-compatible metadata.</p>"},{"location":"guide/export/#cli-usage_1","title":"CLI usage","text":"<pre><code>planopticon export obsidian DB_PATH [OPTIONS]\n</code></pre> <p>Options:</p> Option Short Default Description <code>--output</code> <code>-o</code> <code>./obsidian-vault</code> Output vault directory <p>Example:</p> <pre><code>planopticon export obsidian knowledge_graph.db -o ./my-vault\n</code></pre>"},{"location":"guide/export/#generated-structure","title":"Generated structure","text":"<pre><code>my-vault/\n _Index.md # Map of Content (MOC)\n Tag - Person.md # One tag page per entity type\n Tag - Technology.md\n Tag - Concept.md\n Alice.md # Individual entity notes\n Python.md\n Microservices.md\n ...\n</code></pre>"},{"location":"guide/export/#entity-notes","title":"Entity notes","text":"<p>Each entity gets a dedicated note with:</p> <p>YAML frontmatter:</p> <pre><code>---\ntype: technology\ntags:\n - technology\naliases:\n - Python 3\n - CPython\ndate: 2026-03-07\n---\n</code></pre> <p>The frontmatter includes:</p> <ul> <li><code>type</code>: The entity type</li> <li><code>tags</code>: Entity type as a tag (for Obsidian tag-based filtering)</li> <li><code>aliases</code>: Any known aliases for the entity (if available)</li> <li><code>date</code>: The export date</li> </ul> <p>Body content:</p> <ul> <li><code># Entity Name</code> heading</li> <li>Description paragraphs</li> <li><code>## Relationships</code> section with <code>[[wiki-links]]</code> to related entities: <pre><code>- **uses**: [[FastAPI]]\n- **depends_on**: [[PostgreSQL]]\n</code></pre></li> <li><code>## Referenced by</code> section with incoming relationships: <pre><code>- **implements** from [[Backend Service]]\n</code></pre></li> </ul>"},{"location":"guide/export/#index-note-map-of-content","title":"Index note (Map of Content)","text":"<p>The <code>_Index.md</code> file serves as a Map of Content (MOC), listing all entities grouped by type with <code>[[wiki-links]]</code>:</p> <pre><code>---\ntype: index\ntags:\n - MOC\ndate: 2026-03-07\n---\n\n# Index\n\n**47** entities | **31** relationships\n\n## Concept\n\n- [[Microservices]]\n- [[REST API]]\n\n## Person\n\n- [[Alice]]\n- [[Bob]]\n</code></pre>"},{"location":"guide/export/#tag-pages","title":"Tag pages","text":"<p>One tag page is created per entity type (e.g., <code>Tag - Person.md</code>, <code>Tag - Technology.md</code>). Each page has frontmatter tagging it with the entity type and lists all entities of that type with descriptions.</p>"},{"location":"guide/export/#notion-compatible-markdown-export","title":"Notion-compatible markdown export","text":"<p>The Notion exporter creates Markdown files with Notion-style callout blocks and a CSV database file for bulk import into Notion.</p>"},{"location":"guide/export/#cli-usage_2","title":"CLI usage","text":"<pre><code>planopticon export notion DB_PATH [OPTIONS]\n</code></pre> <p>Options:</p> Option Short Default Description <code>--output</code> <code>-o</code> <code>./notion-export</code> Output directory <p>Example:</p> <pre><code>planopticon export notion knowledge_graph.db -o ./notion-export\n</code></pre>"},{"location":"guide/export/#generated-structure_1","title":"Generated structure","text":"<pre><code>notion-export/\n Overview.md # Knowledge graph overview page\n entities_database.csv # CSV for Notion database import\n Alice.md # Individual entity pages\n Python.md\n ...\n</code></pre>"},{"location":"guide/export/#entity-pages","title":"Entity pages","text":"<p>Each entity page uses Notion-style callout syntax for metadata:</p> <pre><code># Python\n\n> :computer: **Type:** technology\n\n## Description\n\nA high-level programming language...\n\n> :memo: **Properties**\n> - **version:** 3.11\n> - **paradigm:** multi-paradigm\n\n## Relationships\n\n| Target | Relationship |\n|--------|-------------|\n| FastAPI | uses |\n| Django | framework_for |\n\n## Referenced by\n\n| Source | Relationship |\n|--------|-------------|\n| Backend Service | implements |\n</code></pre>"},{"location":"guide/export/#csv-database","title":"CSV database","text":"<p>The <code>entities_database.csv</code> file contains all entities in a format suitable for Notion's CSV database import:</p> Column Description Name Entity name Type Entity type Description First two descriptions, semicolon-separated Related To Comma-separated list of outgoing relationship targets"},{"location":"guide/export/#overview-page","title":"Overview page","text":"<p>The <code>Overview.md</code> page provides a summary with entity counts and a grouped listing of all entities by type.</p>"},{"location":"guide/export/#github-wiki-generator","title":"GitHub wiki generator","text":"<p>The wiki generator creates a complete set of GitHub wiki pages from a knowledge graph, including navigation (Home page and Sidebar) and cross-linked entity pages.</p>"},{"location":"guide/export/#cli-usage_3","title":"CLI usage","text":"<p>Generate wiki pages locally:</p> <pre><code>planopticon wiki generate DB_PATH [OPTIONS]\n</code></pre> Option Short Default Description <code>--output</code> <code>-o</code> <code>./wiki</code> Output directory for wiki pages <code>--title</code> <code>Knowledge Base</code> Wiki title (shown on Home page) <p>Push wiki pages to GitHub:</p> <pre><code>planopticon wiki push WIKI_DIR REPO [OPTIONS]\n</code></pre> Argument Description <code>WIKI_DIR</code> Path to the directory containing generated wiki <code>.md</code> files <code>REPO</code> GitHub repository in <code>owner/repo</code> format Option Short Default Description <code>--message</code> <code>-m</code> <code>Update wiki</code> Git commit message <p>Examples:</p> <pre><code># Generate wiki pages\nplanopticon wiki generate knowledge_graph.db -o ./wiki\n\n# Generate with a custom title\nplanopticon wiki generate kg.db -o ./wiki --title \"Project Wiki\"\n\n# Push to GitHub\nplanopticon wiki push ./wiki ConflictHQ/PlanOpticon\n\n# Push with a custom commit message\nplanopticon wiki push ./wiki owner/repo -m \"Add entity pages\"\n</code></pre>"},{"location":"guide/export/#generated-pages","title":"Generated pages","text":"<p>The wiki generator creates the following pages:</p> Page Description <code>Home.md</code> Main wiki page with entity counts, type links, and artifact links <code>_Sidebar.md</code> Navigation sidebar with links to Home, entity type indexes, and artifacts <code>{Type}.md</code> One index page per entity type with a table of entities and descriptions <code>{Entity}.md</code> Individual entity pages with type, descriptions, relationships, and sources"},{"location":"guide/export/#entity-pages_1","title":"Entity pages","text":"<p>Each entity page contains:</p> <ul> <li>Entity name as the top heading</li> <li>Type label</li> <li>Descriptions section (bullet list)</li> <li>Relationships table with wiki-style links to target entities</li> <li>Referenced By table with links to source entities</li> <li>Sources section listing occurrences with timestamps and context</li> </ul> <p>All entity and type names are cross-linked using GitHub wiki-compatible links (<code>[Name](Sanitized-Name)</code>).</p>"},{"location":"guide/export/#push-behavior","title":"Push behavior","text":"<p>The <code>wiki push</code> command:</p> <ol> <li>Clones the existing GitHub wiki repository (<code>https://github.com/{repo}.wiki.git</code>).</li> <li>If the wiki does not exist yet, initializes a new Git repository.</li> <li>Copies all <code>.md</code> files from the wiki directory into the clone.</li> <li>Commits the changes.</li> <li>Pushes to the remote (tries <code>master</code> first, then <code>main</code>).</li> </ol> <p>This requires Git authentication with push access to the repository. The wiki must be enabled in the GitHub repository settings.</p>"},{"location":"guide/export/#planopticonexchange-json-format","title":"PlanOpticonExchange JSON format","text":"<p>The PlanOpticonExchange is the canonical interchange format for PlanOpticon data. Every command produces it, and every export adapter can consume it. It provides a structured, versioned JSON representation of a complete knowledge graph with project metadata.</p>"},{"location":"guide/export/#cli-usage_4","title":"CLI usage","text":"<pre><code>planopticon export exchange DB_PATH [OPTIONS]\n</code></pre> Option Short Default Description <code>--output</code> <code>-o</code> <code>./exchange.json</code> Output JSON file path <code>--name</code> <code>Untitled</code> Project name for the exchange payload <code>--description</code> (empty) Project description <p>Examples:</p> <pre><code># Basic export\nplanopticon export exchange knowledge_graph.db\n\n# With project metadata\nplanopticon export exchange kg.db -o exchange.json --name \"My Project\" --description \"Sprint 3 analysis\"\n</code></pre>"},{"location":"guide/export/#schema","title":"Schema","text":"<p>The exchange format has the following top-level structure:</p> <pre><code>{\n \"version\": \"1.0\",\n \"project\": {\n \"name\": \"My Project\",\n \"description\": \"Sprint 3 analysis\",\n \"created_at\": \"2026-03-07T10:30:00.000000\",\n \"updated_at\": \"2026-03-07T10:30:00.000000\",\n \"tags\": [\"sprint-3\", \"backend\"]\n },\n \"entities\": [\n {\n \"name\": \"Python\",\n \"type\": \"technology\",\n \"descriptions\": [\"A high-level programming language\"],\n \"source\": \"transcript\",\n \"occurrences\": [\n {\n \"source\": \"meeting.mp4\",\n \"timestamp\": \"00:05:23\",\n \"text\": \"We should use Python for the backend\"\n }\n ]\n }\n ],\n \"relationships\": [\n {\n \"source\": \"Python\",\n \"target\": \"Backend Service\",\n \"type\": \"used_by\",\n \"content_source\": \"transcript:meeting.mp4\",\n \"timestamp\": 323.0\n }\n ],\n \"artifacts\": [\n {\n \"name\": \"Project Plan\",\n \"content\": \"# Project Plan\\n\\n...\",\n \"artifact_type\": \"project_plan\",\n \"format\": \"markdown\",\n \"metadata\": {}\n }\n ],\n \"sources\": [\n {\n \"source_id\": \"abc123\",\n \"source_type\": \"video\",\n \"title\": \"Sprint Planning Meeting\",\n \"path\": \"/recordings/meeting.mp4\",\n \"url\": null,\n \"mime_type\": \"video/mp4\",\n \"ingested_at\": \"2026-03-07T10:00:00.000000\",\n \"metadata\": {}\n }\n ]\n}\n</code></pre> <p>Top-level fields:</p> Field Type Description <code>version</code> <code>str</code> Schema version (currently <code>\"1.0\"</code>) <code>project</code> <code>ProjectMeta</code> Project-level metadata <code>entities</code> <code>List[Entity]</code> Knowledge graph entities <code>relationships</code> <code>List[Relationship]</code> Knowledge graph relationships <code>artifacts</code> <code>List[ArtifactMeta]</code> Generated artifacts (plans, PRDs, etc.) <code>sources</code> <code>List[SourceRecord]</code> Content source provenance records"},{"location":"guide/export/#merging-exchange-files","title":"Merging exchange files","text":"<p>The exchange format supports merging, with automatic deduplication:</p> <ul> <li>Entities are deduplicated by name</li> <li>Relationships are deduplicated by the tuple <code>(source, target, type)</code></li> <li>Artifacts are deduplicated by name</li> <li>Sources are deduplicated by <code>source_id</code></li> </ul> <pre><code>from video_processor.exchange import PlanOpticonExchange\n\n# Load two exchange files\nex1 = PlanOpticonExchange.from_file(\"sprint-1.json\")\nex2 = PlanOpticonExchange.from_file(\"sprint-2.json\")\n\n# Merge ex2 into ex1\nex1.merge(ex2)\n\n# Save the combined result\nex1.to_file(\"combined.json\")\n</code></pre> <p>The <code>project.updated_at</code> timestamp is updated automatically on merge.</p>"},{"location":"guide/export/#python-api","title":"Python API","text":"<p>Create from a knowledge graph:</p> <pre><code>from video_processor.exchange import PlanOpticonExchange\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\n\nkg = KnowledgeGraph(db_path=\"knowledge_graph.db\")\nkg_data = kg.to_dict()\n\nexchange = PlanOpticonExchange.from_knowledge_graph(\n kg_data,\n project_name=\"My Project\",\n project_description=\"Analysis of sprint planning meetings\",\n tags=[\"planning\", \"backend\"],\n)\n</code></pre> <p>Save and load:</p> <pre><code># Save to file\nexchange.to_file(\"exchange.json\")\n\n# Load from file\nloaded = PlanOpticonExchange.from_file(\"exchange.json\")\n</code></pre> <p>Get JSON Schema:</p> <pre><code>schema = PlanOpticonExchange.json_schema()\n</code></pre> <p>This returns the full JSON Schema for validation and documentation purposes.</p>"},{"location":"guide/export/#python-api-for-all-exporters","title":"Python API for all exporters","text":""},{"location":"guide/export/#markdown-document-generation","title":"Markdown document generation","text":"<pre><code>from pathlib import Path\nfrom video_processor.exporters.markdown import (\n generate_all,\n generate_executive_summary,\n generate_meeting_notes,\n generate_glossary,\n generate_relationship_map,\n generate_status_report,\n generate_entity_index,\n generate_csv_export,\n generate_entity_brief,\n DOCUMENT_TYPES,\n)\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nkg_data = kg.to_dict()\n\n# Generate all document types at once\ncreated_files = generate_all(kg_data, Path(\"./export\"))\n\n# Generate specific document types\ncreated_files = generate_all(\n kg_data,\n Path(\"./export\"),\n doc_types=[\"summary\", \"glossary\", \"csv\"],\n)\n\n# Generate individual documents (returns markdown string)\nsummary = generate_executive_summary(kg_data)\nnotes = generate_meeting_notes(kg_data, title=\"Sprint Planning\")\nglossary = generate_glossary(kg_data)\nrel_map = generate_relationship_map(kg_data)\nstatus = generate_status_report(kg_data, title=\"Q1 Status\")\nindex = generate_entity_index(kg_data)\ncsv_text = generate_csv_export(kg_data)\n\n# Generate a brief for a single entity\nentity = kg_data[\"nodes\"][0]\nrelationships = kg_data[\"relationships\"]\nbrief = generate_entity_brief(entity, relationships)\n</code></pre>"},{"location":"guide/export/#obsidian-export","title":"Obsidian export","text":"<pre><code>from pathlib import Path\nfrom video_processor.agent.skills.notes_export import export_to_obsidian\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nkg_data = kg.to_dict()\n\ncreated_files = export_to_obsidian(kg_data, Path(\"./obsidian-vault\"))\nprint(f\"Created {len(created_files)} files\")\n</code></pre>"},{"location":"guide/export/#notion-export","title":"Notion export","text":"<pre><code>from pathlib import Path\nfrom video_processor.agent.skills.notes_export import export_to_notion_md\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nkg_data = kg.to_dict()\n\ncreated_files = export_to_notion_md(kg_data, Path(\"./notion-export\"))\n</code></pre>"},{"location":"guide/export/#wiki-generation","title":"Wiki generation","text":"<pre><code>from pathlib import Path\nfrom video_processor.agent.skills.wiki_generator import (\n generate_wiki,\n write_wiki,\n push_wiki,\n)\nfrom video_processor.integrators.knowledge_graph import KnowledgeGraph\n\nkg = KnowledgeGraph(db_path=Path(\"knowledge_graph.db\"))\nkg_data = kg.to_dict()\n\n# Generate pages as a dict of {filename: content}\npages = generate_wiki(kg_data, title=\"Project Wiki\")\n\n# Write to disk\nwritten = write_wiki(pages, Path(\"./wiki\"))\n\n# Push to GitHub (requires git auth)\nsuccess = push_wiki(Path(\"./wiki\"), \"owner/repo\", message=\"Update wiki\")\n</code></pre>"},{"location":"guide/export/#companion-repl","title":"Companion REPL","text":"<p>Inside the interactive companion REPL, use the <code>/export</code> command:</p> <pre><code>> /export markdown\nExport 'markdown' requested. Use the CLI command:\n planopticon export markdown ./knowledge_graph.db\n\n> /export obsidian\nExport 'obsidian' requested. Use the CLI command:\n planopticon export obsidian ./knowledge_graph.db\n</code></pre> <p>The REPL provides guidance on the CLI command to run; actual export is performed via the CLI.</p>"},{"location":"guide/export/#common-workflows","title":"Common workflows","text":""},{"location":"guide/export/#analyze-videos-and-export-to-obsidian","title":"Analyze videos and export to Obsidian","text":"<pre><code># Analyze meeting recordings\nplanopticon analyze meeting-1.mp4 -o ./results\nplanopticon analyze meeting-2.mp4 --db-path ./results/knowledge_graph.db\n\n# Ingest supplementary docs\nplanopticon ingest ./specs/ --db-path ./results/knowledge_graph.db\n\n# Export to Obsidian vault\nplanopticon export obsidian ./results/knowledge_graph.db -o ~/Obsidian/ProjectVault\n\n# Open in Obsidian and explore the graph view\n</code></pre>"},{"location":"guide/export/#generate-project-documentation","title":"Generate project documentation","text":"<pre><code># Generate all markdown documents\nplanopticon export markdown knowledge_graph.db -o ./docs\n\n# The output includes:\n# docs/summary.md - Executive summary\n# docs/meeting-notes.md - Meeting notes format\n# docs/glossary.md - Entity glossary\n# docs/relationship-map.md - Relationships + Mermaid diagram\n# docs/status-report.md - Project status report\n# docs/entity-index.md - Master entity index\n# docs/csv.csv - Spreadsheet-ready CSV\n# docs/entities/ - Individual entity briefs\n</code></pre>"},{"location":"guide/export/#publish-a-github-wiki","title":"Publish a GitHub wiki","text":"<pre><code># Generate wiki pages\nplanopticon wiki generate knowledge_graph.db -o ./wiki --title \"Project Knowledge Base\"\n\n# Review locally, then push\nplanopticon wiki push ./wiki ConflictHQ/my-project -m \"Initial wiki from meeting analysis\"\n</code></pre>"},{"location":"guide/export/#share-data-between-projects","title":"Share data between projects","text":"<pre><code># Export from project A\nplanopticon export exchange ./project-a/knowledge_graph.db \\\n -o project-a.json --name \"Project A\"\n\n# Export from project B\nplanopticon export exchange ./project-b/knowledge_graph.db \\\n -o project-b.json --name \"Project B\"\n\n# Merge in Python\npython -c \"\nfrom video_processor.exchange import PlanOpticonExchange\na = PlanOpticonExchange.from_file('project-a.json')\nb = PlanOpticonExchange.from_file('project-b.json')\na.merge(b)\na.to_file('combined.json')\nprint(f'Combined: {len(a.entities)} entities, {len(a.relationships)} relationships')\n\"\n</code></pre>"},{"location":"guide/export/#export-for-spreadsheet-analysis","title":"Export for spreadsheet analysis","text":"<pre><code># Generate just the CSV\nplanopticon export markdown knowledge_graph.db --type csv -o ./export\n\n# The file export/csv.csv can be opened in Excel, Google Sheets, etc.\n</code></pre> <p>Alternatively, the Notion export includes an <code>entities_database.csv</code> that can be imported into any spreadsheet tool or Notion database.</p>"},{"location":"guide/knowledge-graphs/","title":"Knowledge Graphs","text":"<p>PlanOpticon builds structured knowledge graphs from video analyses, document ingestion, and other content sources. A knowledge graph captures entities (people, technologies, concepts, organizations) and the relationships between them, providing a queryable representation of everything discussed or presented in your source material.</p>"},{"location":"guide/knowledge-graphs/#storage","title":"Storage","text":"<p>Knowledge graphs are stored as SQLite databases (<code>knowledge_graph.db</code>) using Python's built-in <code>sqlite3</code> module. This means:</p> <ul> <li>Zero external dependencies. No database server to install or manage.</li> <li>Single-file portability. Copy the <code>.db</code> file to share a knowledge graph.</li> <li>WAL mode. SQLite Write-Ahead Logging is enabled for concurrent read performance.</li> <li>JSON fallback. Knowledge graphs can also be saved as <code>knowledge_graph.json</code> for interoperability, though SQLite is preferred for performance and querying.</li> </ul>"},{"location":"guide/knowledge-graphs/#database-schema","title":"Database Schema","text":"<p>The SQLite store uses the following tables:</p> Table Purpose <code>entities</code> Core entity records with name, type, descriptions, source, and arbitrary properties <code>occurrences</code> Where and when each entity was mentioned (source, timestamp, text snippet) <code>relationships</code> Directed edges between entities with type, content source, timestamp, and properties <code>sources</code> Registered content sources with provenance metadata (source type, title, path, URL, MIME type, ingestion timestamp) <code>source_locations</code> Links between sources and specific entities/relationships, with location details (timestamp, page, section, line range, text snippet) <p>All entity lookups are case-insensitive (indexed on <code>name_lower</code>). Entities and relationships are indexed on their source and target fields for efficient traversal.</p>"},{"location":"guide/knowledge-graphs/#storage-backends","title":"Storage Backends","text":"<p>PlanOpticon supports two storage backends, selected automatically:</p> Backend When Used Persistence <code>SQLiteStore</code> When a <code>db_path</code> is provided Persistent on disk <code>InMemoryStore</code> When no path is given, or as fallback In-memory only <p>Both backends implement the same <code>GraphStore</code> abstract interface, so all query and manipulation code works identically regardless of backend.</p> <pre><code>from video_processor.integrators.graph_store import create_store\n\n# Persistent SQLite store\nstore = create_store(\"/path/to/knowledge_graph.db\")\n\n# In-memory store (for temporary operations)\nstore = create_store()\n</code></pre>"},{"location":"guide/knowledge-graphs/#entity-types","title":"Entity Types","text":"<p>Entities extracted from content are assigned one of the following base types:</p> Type Description Specificity Rank <code>person</code> People mentioned or participating 3 (highest) <code>technology</code> Tools, languages, frameworks, platforms 3 <code>organization</code> Companies, teams, departments 2 <code>time</code> Dates, deadlines, time references 1 <code>diagram</code> Visual diagrams extracted from video frames 1 <code>concept</code> General concepts, topics, ideas (default) 0 (lowest) <p>The specificity rank is used during merge operations: when two entities are matched as duplicates, the more specific type wins (e.g., <code>technology</code> overrides <code>concept</code>).</p>"},{"location":"guide/knowledge-graphs/#planning-taxonomy","title":"Planning Taxonomy","text":"<p>Beyond the base entity types, PlanOpticon includes a planning taxonomy for classifying entities into project-planning categories. The <code>TaxonomyClassifier</code> maps extracted entities into these types:</p> Planning Type Keywords Matched <code>goal</code> goal, objective, aim, target outcome <code>requirement</code> must, should, requirement, need, required <code>constraint</code> constraint, limitation, restrict, cannot, must not <code>decision</code> decided, decision, chose, selected, agreed <code>risk</code> risk, concern, worry, danger, threat <code>assumption</code> assume, assumption, expecting, presume <code>dependency</code> depends, dependency, relies on, prerequisite, blocked <code>milestone</code> milestone, deadline, deliverable, release, launch <code>task</code> task, todo, action item, work item, implement <code>feature</code> feature, capability, functionality <p>Classification works in two stages:</p> <ol> <li>Heuristic classification. Entity descriptions are scanned for the keywords listed above. First match wins.</li> <li>LLM refinement. If an LLM provider is available, entities are sent to the LLM for more nuanced classification with priority assignment (<code>high</code>, <code>medium</code>, <code>low</code>). LLM results override heuristic results on conflicts.</li> </ol> <p>Classified entities are used by planning agent skills (project_plan, prd, roadmap, task_breakdown) to produce targeted, context-aware artifacts.</p>"},{"location":"guide/knowledge-graphs/#relationship-types","title":"Relationship Types","text":"<p>Relationships are directed edges between entities. The <code>type</code> field is a free-text string determined by the LLM during extraction. Common relationship types include:</p> <ul> <li><code>related_to</code> (default)</li> <li><code>works_with</code></li> <li><code>uses</code></li> <li><code>depends_on</code></li> <li><code>proposed</code></li> <li><code>discussed_by</code></li> <li><code>employed_by</code></li> <li><code>collaborates_with</code></li> <li><code>expert_in</code></li> </ul>"},{"location":"guide/knowledge-graphs/#typed-relationships","title":"Typed Relationships","text":"<p>The <code>add_typed_relationship()</code> method creates edges with custom labels and optional properties, enabling richer graph semantics:</p> <pre><code>store.add_typed_relationship(\n source=\"Authentication Service\",\n target=\"PostgreSQL\",\n edge_label=\"USES_SYSTEM\",\n properties={\"purpose\": \"user credential storage\", \"version\": \"15\"},\n)\n</code></pre>"},{"location":"guide/knowledge-graphs/#relationship-checks","title":"Relationship Checks","text":"<p>You can check whether a relationship exists between two entities:</p> <pre><code># Check for any relationship\nstore.has_relationship(\"Alice\", \"Kubernetes\")\n\n# Check for a specific relationship type\nstore.has_relationship(\"Alice\", \"Kubernetes\", edge_label=\"expert_in\")\n</code></pre>"},{"location":"guide/knowledge-graphs/#building-a-knowledge-graph","title":"Building a Knowledge Graph","text":""},{"location":"guide/knowledge-graphs/#from-video-analysis","title":"From Video Analysis","text":"<p>The primary path for building a knowledge graph is through video analysis. When you run <code>planopticon analyze</code>, the pipeline extracts entities and relationships from:</p> <ul> <li>Transcript segments -- batched in groups of 10 for efficient API usage, with speaker identification</li> <li>Diagram content -- text extracted from visual diagrams detected in video frames</li> </ul> <pre><code>planopticon analyze -i meeting.mp4 -o results/\n# Creates results/knowledge_graph.db\n</code></pre>"},{"location":"guide/knowledge-graphs/#from-document-ingestion","title":"From Document Ingestion","text":"<p>Documents (Markdown, PDF, DOCX) can be ingested directly into a knowledge graph:</p> <pre><code># Ingest a single file\nplanopticon ingest -i requirements.pdf -o results/\n\n# Ingest a directory recursively\nplanopticon ingest -i docs/ -o results/ --recursive\n\n# Ingest into an existing knowledge graph\nplanopticon ingest -i notes.md --db results/knowledge_graph.db\n</code></pre>"},{"location":"guide/knowledge-graphs/#from-batch-processing","title":"From Batch Processing","text":"<p>Multiple videos can be processed in batch mode, with all results merged into a single knowledge graph:</p> <pre><code>planopticon batch -i videos/ -o results/\n</code></pre>"},{"location":"guide/knowledge-graphs/#programmatic-construction","title":"Programmatic Construction","text":"<pre><code>from video_processor.integrators.knowledge_graph import KnowledgeGraph\n\n# Create a new knowledge graph with LLM extraction\nfrom video_processor.providers.manager import ProviderManager\npm = ProviderManager()\nkg = KnowledgeGraph(provider_manager=pm, db_path=\"knowledge_graph.db\")\n\n# Add content (entities and relationships are extracted by LLM)\nkg.add_content(\n text=\"Alice proposed using Kubernetes for container orchestration.\",\n source=\"meeting_notes\",\n timestamp=120.5,\n)\n\n# Process a full transcript\nkg.process_transcript(transcript_data, batch_size=10)\n\n# Process diagram results\nkg.process_diagrams(diagram_results)\n\n# Save\nkg.save(\"knowledge_graph.db\")\n</code></pre>"},{"location":"guide/knowledge-graphs/#merge-and-deduplication","title":"Merge and Deduplication","text":"<p>When combining knowledge graphs from multiple sources, PlanOpticon performs intelligent merge with deduplication.</p>"},{"location":"guide/knowledge-graphs/#fuzzy-name-matching","title":"Fuzzy Name Matching","text":"<p>Entity names are compared using Python's <code>SequenceMatcher</code> with a threshold of 0.85. This means \"Kubernetes\" and \"kubernetes\" are matched exactly (case-insensitive), while \"React.js\" and \"ReactJS\" may be matched as duplicates if their similarity ratio meets the threshold.</p>"},{"location":"guide/knowledge-graphs/#type-conflict-resolution","title":"Type Conflict Resolution","text":"<p>When two entities match but have different types, the more specific type wins based on the specificity ranking:</p> Scenario Result <code>concept</code> vs <code>technology</code> <code>technology</code> wins (rank 3 > rank 0) <code>person</code> vs <code>concept</code> <code>person</code> wins (rank 3 > rank 0) <code>organization</code> vs <code>concept</code> <code>organization</code> wins (rank 2 > rank 0) <code>person</code> vs <code>technology</code> Keeps whichever was first (equal rank)"},{"location":"guide/knowledge-graphs/#provenance-tracking","title":"Provenance Tracking","text":"<p>Merged entities receive a <code>merged_from:<original_name></code> description entry, preserving the audit trail of which entities were unified.</p>"},{"location":"guide/knowledge-graphs/#programmatic-merge","title":"Programmatic Merge","text":"<pre><code>from video_processor.integrators.knowledge_graph import KnowledgeGraph\n\n# Load two knowledge graphs\nkg1 = KnowledgeGraph(db_path=\"project_a.db\")\nkg2 = KnowledgeGraph(db_path=\"project_b.db\")\n\n# Merge kg2 into kg1\nkg1.merge(kg2)\n\n# Save the merged result\nkg1.save(\"merged.db\")\n</code></pre> <p>The merge operation also copies all registered sources and occurrences, so provenance information is preserved across merges.</p>"},{"location":"guide/knowledge-graphs/#querying","title":"Querying","text":"<p>PlanOpticon provides two query modes: direct mode (no LLM required) and agentic mode (LLM-powered natural language).</p>"},{"location":"guide/knowledge-graphs/#direct-mode","title":"Direct Mode","text":"<p>Direct mode queries are fast, deterministic, and require no API key. They are the right choice for structured lookups.</p>"},{"location":"guide/knowledge-graphs/#stats","title":"Stats","text":"<p>Return entity count, relationship count, and entity type breakdown:</p> <pre><code>planopticon query\n</code></pre> <pre><code>engine.stats()\n# QueryResult with data: {\n# \"entity_count\": 42,\n# \"relationship_count\": 87,\n# \"entity_types\": {\"technology\": 15, \"person\": 12, ...}\n# }\n</code></pre>"},{"location":"guide/knowledge-graphs/#entities","title":"Entities","text":"<p>Filter entities by name substring and/or type:</p> <pre><code>planopticon query \"entities --type technology\"\nplanopticon query \"entities --name python\"\n</code></pre> <pre><code>engine.entities(entity_type=\"technology\")\nengine.entities(name=\"python\")\nengine.entities(name=\"auth\", entity_type=\"concept\", limit=10)\n</code></pre> <p>All filtering is case-insensitive. Results are capped at 50 by default (configurable via <code>limit</code>).</p>"},{"location":"guide/knowledge-graphs/#neighbors","title":"Neighbors","text":"<p>Get an entity and all directly connected nodes and relationships:</p> <pre><code>planopticon query \"neighbors Alice\"\n</code></pre> <pre><code>engine.neighbors(\"Alice\", depth=1)\n</code></pre> <p>The <code>depth</code> parameter controls how many hops to traverse (default 1). The result includes both entity objects and relationship objects.</p>"},{"location":"guide/knowledge-graphs/#relationships","title":"Relationships","text":"<p>Filter relationships by source, target, and/or type:</p> <pre><code>planopticon query \"relationships --source Alice\"\n</code></pre> <pre><code>engine.relationships(source=\"Alice\")\nengine.relationships(target=\"Kubernetes\", rel_type=\"uses\")\n</code></pre>"},{"location":"guide/knowledge-graphs/#sources","title":"Sources","text":"<p>List all registered content sources:</p> <pre><code>engine.sources()\n</code></pre>"},{"location":"guide/knowledge-graphs/#provenance","title":"Provenance","text":"<p>Get all source locations for a specific entity, showing exactly where it was mentioned:</p> <pre><code>engine.provenance(\"Kubernetes\")\n# Returns source locations with timestamps, pages, sections, and text snippets\n</code></pre>"},{"location":"guide/knowledge-graphs/#raw-sql","title":"Raw SQL","text":"<p>Execute arbitrary SQL against the SQLite backend (SQLite stores only):</p> <pre><code>engine.sql(\"SELECT name, type FROM entities WHERE type = 'technology' ORDER BY name\")\n</code></pre>"},{"location":"guide/knowledge-graphs/#agentic-mode","title":"Agentic Mode","text":"<p>Agentic mode accepts natural-language questions and uses the LLM to plan and execute queries. It requires a configured LLM provider.</p> <pre><code>planopticon query \"What technologies were discussed?\"\nplanopticon query \"Who are the key people mentioned?\"\nplanopticon query \"What depends on the authentication service?\"\n</code></pre> <p>The agentic query pipeline:</p> <ol> <li>Plan. The LLM receives graph stats and available actions (entities, relationships, neighbors, stats). It selects exactly one action and its parameters.</li> <li>Execute. The chosen action is run through the direct-mode engine.</li> <li>Synthesize. The LLM receives the raw query results and the original question, then produces a concise natural-language answer.</li> </ol> <p>This design ensures the LLM never generates arbitrary code -- it only selects from a fixed set of known query actions.</p> <pre><code># Requires an API key\nplanopticon query \"What technologies were discussed?\" -p openai\n\n# Use the interactive REPL for multiple queries\nplanopticon query -I\n</code></pre>"},{"location":"guide/knowledge-graphs/#graph-query-engine-python-api","title":"Graph Query Engine Python API","text":"<p>The <code>GraphQueryEngine</code> class provides the programmatic interface for all query operations.</p>"},{"location":"guide/knowledge-graphs/#initialization","title":"Initialization","text":"<pre><code>from video_processor.integrators.graph_query import GraphQueryEngine\nfrom video_processor.integrators.graph_discovery import find_nearest_graph\n\n# From a .db file\npath = find_nearest_graph()\nengine = GraphQueryEngine.from_db_path(path)\n\n# From a .json file\nengine = GraphQueryEngine.from_json_path(\"knowledge_graph.json\")\n\n# With an LLM provider for agentic mode\nfrom video_processor.providers.manager import ProviderManager\npm = ProviderManager()\nengine = GraphQueryEngine.from_db_path(path, provider_manager=pm)\n</code></pre>"},{"location":"guide/knowledge-graphs/#queryresult","title":"QueryResult","text":"<p>All query methods return a <code>QueryResult</code> dataclass with multiple output formats:</p> <pre><code>result = engine.stats()\n\n# Human-readable text\nprint(result.to_text())\n\n# JSON string\nprint(result.to_json())\n\n# Mermaid diagram (for graph results)\nresult = engine.neighbors(\"Alice\")\nprint(result.to_mermaid())\n</code></pre> <p>The <code>QueryResult</code> contains:</p> Field Type Description <code>data</code> Any The raw result data (dict, list, or scalar) <code>query_type</code> str <code>\"filter\"</code> for direct mode, <code>\"agentic\"</code> for LLM mode, <code>\"sql\"</code> for raw SQL <code>raw_query</code> str String representation of the executed query <code>explanation</code> str Human-readable explanation or LLM-synthesized answer"},{"location":"guide/knowledge-graphs/#the-self-contained-html-viewer","title":"The Self-Contained HTML Viewer","text":"<p>PlanOpticon includes a zero-dependency HTML knowledge graph viewer at <code>knowledge-base/viewer.html</code>. This file is fully self-contained -- it inlines D3.js and requires no build step, no server, and no internet connection.</p> <p>To use it, open <code>viewer.html</code> in a browser. It will load and visualize a <code>knowledge_graph.json</code> file (place it in the same directory, or use the file picker in the viewer).</p> <p>The viewer provides:</p> <ul> <li>Interactive force-directed graph layout</li> <li>Zoom and pan navigation</li> <li>Entity nodes colored by type</li> <li>Relationship edges with labels</li> <li>Click-to-focus on individual entities</li> <li>Entity detail panel showing descriptions and connections</li> </ul> <p>This covers approximately 80% of graph exploration needs with zero infrastructure.</p>"},{"location":"guide/knowledge-graphs/#kg-management-commands","title":"KG Management Commands","text":"<p>The <code>planopticon kg</code> command group provides utilities for managing knowledge graph files.</p>"},{"location":"guide/knowledge-graphs/#kg-convert","title":"kg convert","text":"<p>Convert a knowledge graph between SQLite and JSON formats:</p> <pre><code># SQLite to JSON\nplanopticon kg convert results/knowledge_graph.db output.json\n\n# JSON to SQLite\nplanopticon kg convert knowledge_graph.json knowledge_graph.db\n</code></pre> <p>The output format is inferred from the destination file extension. Source and destination must be different formats.</p>"},{"location":"guide/knowledge-graphs/#kg-sync","title":"kg sync","text":"<p>Synchronize a <code>.db</code> and <code>.json</code> knowledge graph, updating the stale one:</p> <pre><code># Auto-detect which is newer and sync\nplanopticon kg sync results/knowledge_graph.db\n\n# Explicit JSON path\nplanopticon kg sync knowledge_graph.db knowledge_graph.json\n\n# Force a specific direction\nplanopticon kg sync knowledge_graph.db knowledge_graph.json --direction db-to-json\nplanopticon kg sync knowledge_graph.db knowledge_graph.json --direction json-to-db\n</code></pre> <p>If <code>JSON_PATH</code> is omitted, the <code>.json</code> path is derived from the <code>.db</code> path (same name, different extension). In <code>auto</code> mode (the default), the newer file is used as the source.</p>"},{"location":"guide/knowledge-graphs/#kg-inspect","title":"kg inspect","text":"<p>Show summary statistics for a knowledge graph file:</p> <pre><code>planopticon kg inspect results/knowledge_graph.db\n</code></pre> <p>Output:</p> <pre><code>File: results/knowledge_graph.db\nStore: sqlite\nEntities: 42\nRelationships: 87\nEntity types:\n technology: 15\n person: 12\n concept: 10\n organization: 5\n</code></pre> <p>Works with both <code>.db</code> and <code>.json</code> files.</p>"},{"location":"guide/knowledge-graphs/#kg-classify","title":"kg classify","text":"<p>Classify knowledge graph entities into planning taxonomy types:</p> <pre><code># Heuristic + LLM classification\nplanopticon kg classify results/knowledge_graph.db\n\n# Heuristic only (no API key needed)\nplanopticon kg classify results/knowledge_graph.db -p none\n\n# JSON output\nplanopticon kg classify results/knowledge_graph.db --format json\n</code></pre> <p>Text output groups entities by planning type:</p> <pre><code>GOALS (3)\n - Improve system reliability [high]\n Must achieve 99.9% uptime\n - Reduce deployment time [medium]\n Automate the deployment pipeline\n\nRISKS (2)\n - Data migration complexity [high]\n Legacy schema incompatibilities\n ...\n\nTASKS (5)\n - Implement OAuth2 flow\n Set up authentication service\n ...\n</code></pre> <p>JSON output returns an array of <code>PlanningEntity</code> objects with <code>name</code>, <code>planning_type</code>, <code>priority</code>, <code>description</code>, and <code>source_entities</code> fields.</p>"},{"location":"guide/knowledge-graphs/#kg-from-exchange","title":"kg from-exchange","text":"<p>Import a PlanOpticonExchange JSON file into a knowledge graph database:</p> <pre><code># Import to default location (./knowledge_graph.db)\nplanopticon kg from-exchange exchange.json\n\n# Import to a specific path\nplanopticon kg from-exchange exchange.json -o project.db\n</code></pre> <p>The PlanOpticonExchange format is a standardized interchange format that includes entities, relationships, and source records.</p>"},{"location":"guide/knowledge-graphs/#output-formats","title":"Output Formats","text":"<p>Query results can be output in three formats:</p>"},{"location":"guide/knowledge-graphs/#text-default","title":"Text (default)","text":"<p>Human-readable format with entity types in brackets, relationship arrows, and indented details:</p> <pre><code>Found 15 entities\n [technology] Python -- General-purpose programming language\n [person] Alice -- Lead engineer on the project\n [concept] Microservices -- Architectural pattern discussed\n</code></pre>"},{"location":"guide/knowledge-graphs/#json","title":"JSON","text":"<p>Full structured output including query metadata:</p> <pre><code>planopticon query --format json stats\n</code></pre> <pre><code>{\n \"query_type\": \"filter\",\n \"raw_query\": \"stats()\",\n \"explanation\": \"Knowledge graph statistics\",\n \"data\": {\n \"entity_count\": 42,\n \"relationship_count\": 87,\n \"entity_types\": {\n \"technology\": 15,\n \"person\": 12\n }\n }\n}\n</code></pre>"},{"location":"guide/knowledge-graphs/#mermaid","title":"Mermaid","text":"<p>Graph results rendered as Mermaid diagram syntax, ready for embedding in markdown:</p> <pre><code>planopticon query --format mermaid \"neighbors Alice\"\n</code></pre> <pre><code>graph LR\n Alice[\"Alice\"]:::person\n Python[\"Python\"]:::technology\n Kubernetes[\"Kubernetes\"]:::technology\n Alice -- \"expert_in\" --> Kubernetes\n Alice -- \"works_with\" --> Python\n classDef person fill:#f9d5e5,stroke:#333\n classDef concept fill:#eeeeee,stroke:#333\n classDef technology fill:#d5e5f9,stroke:#333\n classDef organization fill:#f9e5d5,stroke:#333\n</code></pre> <p>The <code>KnowledgeGraph.generate_mermaid()</code> method also produces full-graph Mermaid diagrams, capped at the top 30 most-connected nodes by default.</p>"},{"location":"guide/knowledge-graphs/#auto-discovery","title":"Auto-Discovery","text":"<p>PlanOpticon automatically locates knowledge graph files using the <code>find_nearest_graph()</code> function. The search order is:</p> <ol> <li>Current directory -- check for <code>knowledge_graph.db</code> and <code>knowledge_graph.json</code></li> <li>Common subdirectories -- <code>results/</code>, <code>output/</code>, <code>knowledge-base/</code></li> <li>Recursive downward walk -- up to 4 levels deep, skipping hidden directories</li> <li>Parent directory walk -- upward through the directory tree, checking each level and its common subdirectories</li> </ol> <p>Within each search phase, <code>.db</code> files are preferred over <code>.json</code> files. Results are sorted by proximity (closest first).</p> <pre><code>from video_processor.integrators.graph_discovery import (\n find_nearest_graph,\n find_knowledge_graphs,\n describe_graph,\n)\n\n# Find the single closest knowledge graph\npath = find_nearest_graph()\n\n# Find all knowledge graphs, sorted by proximity\npaths = find_knowledge_graphs()\n\n# Find graphs starting from a specific directory\npaths = find_knowledge_graphs(start_dir=\"/path/to/project\")\n\n# Disable upward walking\npaths = find_knowledge_graphs(walk_up=False)\n\n# Get summary stats without loading the full graph\ninfo = describe_graph(path)\n# {\"entity_count\": 42, \"relationship_count\": 87,\n# \"entity_types\": {...}, \"store_type\": \"sqlite\"}\n</code></pre> <p>Auto-discovery is used by the Companion REPL, the <code>planopticon query</code> command, and the planning agent when no explicit <code>--kb</code> path is provided.</p>"},{"location":"guide/output-formats/","title":"Output Formats","text":"<p>PlanOpticon produces a wide range of output formats from video analysis, document ingestion, batch processing, knowledge graph export, and agent skills. This page is the comprehensive reference for every format the tool can emit.</p>"},{"location":"guide/output-formats/#transcripts","title":"Transcripts","text":"<p>Video analysis always produces transcripts in three formats, stored in the <code>transcript/</code> subdirectory of the output folder.</p> Format File Description JSON <code>transcript/transcript.json</code> Full transcript with segments, timestamps, speaker labels, and confidence scores. Each segment includes <code>start</code>, <code>end</code>, <code>text</code>, and optional <code>speaker</code> fields. Text <code>transcript/transcript.txt</code> Plain text transcript with no metadata. Suitable for feeding into other tools or reading directly. SRT <code>transcript/transcript.srt</code> SubRip subtitle format with sequential numbering and <code>HH:MM:SS,mmm</code> timestamps. Can be loaded into video players or subtitle editors."},{"location":"guide/output-formats/#transcript-json-structure","title":"Transcript JSON structure","text":"<pre><code>{\n \"segments\": [\n {\n \"start\": 0.0,\n \"end\": 4.5,\n \"text\": \"Welcome to the sprint review.\",\n \"speaker\": \"Alice\"\n }\n ],\n \"text\": \"Welcome to the sprint review. ...\",\n \"language\": \"en\"\n}\n</code></pre> <p>When the <code>--speakers</code> flag is provided (e.g., <code>--speakers \"Alice,Bob,Carol\"</code>), speaker diarization hints are passed to the transcription provider and speaker labels appear in the JSON segments.</p>"},{"location":"guide/output-formats/#reports","title":"Reports","text":"<p>Analysis reports are generated from the combined transcript, diagrams, key points, action items, and knowledge graph. They live in the <code>results/</code> subdirectory.</p> Format File Description Markdown <code>results/analysis.md</code> Structured report with embedded Mermaid diagram blocks, tables, and cross-references. Works in any Markdown renderer. HTML <code>results/analysis.html</code> Self-contained HTML page with inline CSS, embedded SVG diagrams, and a bundled mermaid.js script for rendering any unrendered Mermaid blocks. No external dependencies required to view. PDF <code>results/analysis.pdf</code> Print-ready PDF. Requires the <code>planopticon[pdf]</code> extra (<code>pip install planopticon[pdf]</code>). Generated from the HTML report."},{"location":"guide/output-formats/#diagrams","title":"Diagrams","text":"<p>Each visual element detected during frame analysis produces up to five output files in the <code>diagrams/</code> subdirectory. The index <code>N</code> is zero-based.</p> Format File Description JPEG <code>diagrams/diagram_N.jpg</code> Original video frame captured at the point of detection. Mermaid <code>diagrams/diagram_N.mermaid</code> Mermaid source code reconstructed from the diagram by the vision model. Supports flowcharts, sequence diagrams, architecture diagrams, and more. SVG <code>diagrams/diagram_N.svg</code> Vector rendering of the Mermaid source, produced by the Mermaid CLI or built-in renderer. PNG <code>diagrams/diagram_N.png</code> Raster rendering of the Mermaid source at high resolution. JSON <code>diagrams/diagram_N.json</code> Structured analysis data including diagram type, description, extracted text, chart data (if applicable), and confidence score. <p>Frames that score as medium confidence are saved as captioned screenshots in the <code>captures/</code> subdirectory instead, with a <code>capture_N.jpg</code> and <code>capture_N.json</code> pair.</p>"},{"location":"guide/output-formats/#structured-data","title":"Structured Data","text":"<p>Core analysis artifacts are stored as JSON files in the <code>results/</code> subdirectory.</p> Format File Description SQLite <code>results/knowledge_graph.db</code> Primary knowledge graph database. SQLite-based, queryable with <code>planopticon query</code>. Contains entities, relationships, source provenance, and metadata. This is the preferred format for querying and merging. JSON <code>results/knowledge_graph.json</code> JSON export of the knowledge graph. Contains <code>entities</code> and <code>relationships</code> arrays. Automatically kept in sync with the <code>.db</code> file. Used as a fallback when SQLite is not available. JSON <code>results/key_points.json</code> Array of extracted key points, each with <code>text</code>, <code>category</code>, and <code>confidence</code> fields. JSON <code>results/action_items.json</code> Array of action items, each with <code>text</code>, <code>assignee</code>, <code>due_date</code>, <code>priority</code>, and <code>status</code> fields. JSON <code>manifest.json</code> Complete run manifest. The single source of truth for the analysis run. Contains video metadata, processing stats, file paths to all outputs, and inline key points, action items, diagram metadata, and screen captures."},{"location":"guide/output-formats/#knowledge-graph-json-structure","title":"Knowledge graph JSON structure","text":"<pre><code>{\n \"entities\": [\n {\n \"name\": \"Kubernetes\",\n \"type\": \"technology\",\n \"descriptions\": [\"Container orchestration platform discussed in architecture review\"],\n \"occurrences\": [\n {\"source\": \"video:recording.mp4\", \"timestamp\": \"00:05:23\"}\n ]\n }\n ],\n \"relationships\": [\n {\n \"source\": \"Kubernetes\",\n \"target\": \"Docker\",\n \"type\": \"DEPENDS_ON\",\n \"descriptions\": [\"Kubernetes uses Docker as container runtime\"]\n }\n ]\n}\n</code></pre>"},{"location":"guide/output-formats/#charts","title":"Charts","text":"<p>When chart data is extracted from diagrams (bar charts, line charts, pie charts, scatter plots), PlanOpticon reproduces them as standalone image files.</p> Format File Description SVG <code>diagrams/chart_N.svg</code> Vector chart rendered via matplotlib. Suitable for embedding in documents or scaling to any size. PNG <code>diagrams/chart_N.png</code> Raster chart rendered via matplotlib at 150 DPI. <p>Reproduced charts are also embedded inline in the HTML and PDF reports.</p>"},{"location":"guide/output-formats/#knowledge-graph-exports","title":"Knowledge Graph Exports","text":"<p>Beyond the default <code>knowledge_graph.db</code> and <code>knowledge_graph.json</code> produced during analysis, PlanOpticon supports exporting knowledge graphs to several additional formats via the <code>planopticon export</code> and <code>planopticon kg convert</code> commands.</p> Format Command / File Description JSON <code>knowledge_graph.json</code> Default JSON export. Produced automatically alongside the <code>.db</code> file. SQLite <code>knowledge_graph.db</code> Primary database format. Can be converted to/from JSON with <code>planopticon kg convert</code>. GraphML <code>output.graphml</code> XML-based graph format via <code>planopticon kg convert kg.db output.graphml</code>. Compatible with Gephi, yEd, Cytoscape, and other graph visualization tools. CSV <code>export/entities.csv</code>, <code>export/relationships.csv</code> Tabular export via <code>planopticon export markdown kg.db --type csv</code>. Produces separate CSV files for entities and relationships. Mermaid Inline in reports Mermaid graph diagrams are embedded in Markdown and HTML reports. Also available programmatically via <code>GraphQueryEngine.to_mermaid()</code>."},{"location":"guide/output-formats/#converting-between-formats","title":"Converting between formats","text":"<pre><code># SQLite to JSON\nplanopticon kg convert results/knowledge_graph.db output.json\n\n# JSON to SQLite\nplanopticon kg convert knowledge_graph.json knowledge_graph.db\n\n# Sync both directions (updates the stale file)\nplanopticon kg sync results/knowledge_graph.db\nplanopticon kg sync knowledge_graph.db knowledge_graph.json --direction db-to-json\n</code></pre>"},{"location":"guide/output-formats/#planopticonexchange-format","title":"PlanOpticonExchange Format","text":"<p>The PlanOpticonExchange format (<code>.json</code>) is a canonical interchange payload designed for sharing knowledge graphs between PlanOpticon instances, teams, or external systems.</p> <pre><code>planopticon export exchange knowledge_graph.db\nplanopticon export exchange kg.db -o exchange.json --name \"My Project\"\n</code></pre> <p>The exchange payload includes:</p> <ul> <li>Schema version for forward compatibility</li> <li>Project metadata (name, description)</li> <li>Full entity and relationship data with provenance</li> <li>Source tracking for multi-source graphs</li> <li>Merge support -- exchange files can be merged together, deduplicating entities by name</li> </ul>"},{"location":"guide/output-formats/#exchange-json-structure","title":"Exchange JSON structure","text":"<pre><code>{\n \"schema_version\": \"1.0\",\n \"project\": {\n \"name\": \"Sprint Reviews Q4\",\n \"description\": \"Knowledge extracted from Q4 sprint review recordings\"\n },\n \"entities\": [...],\n \"relationships\": [...],\n \"sources\": [...]\n}\n</code></pre>"},{"location":"guide/output-formats/#document-exports","title":"Document Exports","text":"<p>PlanOpticon can generate structured Markdown documents from any knowledge graph, with no API key required. These are pure template-based outputs derived from the graph data.</p>"},{"location":"guide/output-formats/#markdown-document-types","title":"Markdown document types","text":"<p>There are seven document types plus a CSV export, all generated via <code>planopticon export markdown</code>:</p> Type File Description <code>summary</code> <code>executive_summary.md</code> High-level executive summary with entity counts, top relationships, and key themes. <code>meeting-notes</code> <code>meeting_notes.md</code> Structured meeting notes with attendees, topics discussed, decisions made, and action items. <code>glossary</code> <code>glossary.md</code> Alphabetical glossary of all entities with descriptions and types. <code>relationship-map</code> <code>relationship_map.md</code> Textual and Mermaid-based relationship map showing how entities connect. <code>status-report</code> <code>status_report.md</code> Status report format with progress indicators, risks, and next steps. <code>entity-index</code> <code>entity_index.md</code> Comprehensive index of all entities grouped by type, with links to individual briefs. <code>entity-brief</code> <code>entities/<Name>.md</code> One-pager brief for each entity, showing descriptions, relationships, and source references. <code>csv</code> <code>entities.csv</code> Tabular CSV export of entities and relationships. <pre><code># Generate all document types\nplanopticon export markdown knowledge_graph.db\n\n# Generate specific types\nplanopticon export markdown kg.db -o ./docs --type summary --type glossary\n\n# Generate meeting notes and CSV\nplanopticon export markdown kg.db --type meeting-notes --type csv\n</code></pre>"},{"location":"guide/output-formats/#obsidian-vault-export","title":"Obsidian vault export","text":"<p>Exports the knowledge graph as an Obsidian-compatible vault with YAML frontmatter, <code>[[wiki-links]]</code> between entities, and proper folder structure.</p> <pre><code>planopticon export obsidian knowledge_graph.db -o ./my-vault\n</code></pre> <p>The vault includes:</p> <ul> <li>One note per entity with frontmatter (<code>type</code>, <code>aliases</code>, <code>tags</code>)</li> <li>Wiki-links between related entities</li> <li>A <code>_Index.md</code> file for navigation</li> <li>Compatible with Obsidian graph view</li> </ul>"},{"location":"guide/output-formats/#notion-markdown-export","title":"Notion markdown export","text":"<p>Exports as Notion-compatible Markdown with a CSV database file for import into Notion databases.</p> <pre><code>planopticon export notion knowledge_graph.db -o ./notion-export\n</code></pre>"},{"location":"guide/output-formats/#github-wiki-export","title":"GitHub wiki export","text":"<p>Generates a complete GitHub wiki with a sidebar, home page, and per-entity pages. Can be pushed directly to a GitHub wiki repository.</p> <pre><code># Generate wiki pages\nplanopticon wiki generate knowledge_graph.db -o ./wiki\n\n# Push to GitHub\nplanopticon wiki push ./wiki ConflictHQ/PlanOpticon -m \"Update wiki from KG\"\n</code></pre>"},{"location":"guide/output-formats/#batch-outputs","title":"Batch Outputs","text":"<p>Batch processing produces additional files at the batch root directory, alongside per-video output folders.</p> Format File Description JSON <code>batch_manifest.json</code> Batch-level manifest with aggregate stats, per-video status (completed/failed), error details, and paths to all sub-outputs. Markdown <code>batch_summary.md</code> Aggregated summary report with combined key points, action items, entity counts, and a Mermaid diagram of the merged knowledge graph. SQLite <code>knowledge_graph.db</code> Merged knowledge graph combining entities and relationships across all successfully processed videos. Uses fuzzy matching and conflict resolution. JSON <code>knowledge_graph.json</code> JSON export of the merged knowledge graph."},{"location":"guide/output-formats/#self-contained-html-viewer","title":"Self-Contained HTML Viewer","text":"<p>PlanOpticon ships with a self-contained interactive knowledge graph viewer at <code>knowledge-base/viewer.html</code> in the repository. This file:</p> <ul> <li>Uses D3.js (bundled inline, no CDN dependency)</li> <li>Renders an interactive force-directed graph visualization</li> <li>Supports node filtering by entity type</li> <li>Shows entity details and relationships on click</li> <li>Can load any <code>knowledge_graph.json</code> file</li> <li>Works offline with no server required -- just open in a browser</li> <li>Covers approximately 80% of graph exploration needs with zero infrastructure</li> </ul>"},{"location":"guide/output-formats/#output-directory-structure","title":"Output Directory Structure","text":"<p>A complete single-video analysis produces the following directory tree:</p> <pre><code>output/\n\u251c\u2500\u2500 manifest.json # Run manifest (source of truth)\n\u251c\u2500\u2500 transcript/\n\u2502 \u251c\u2500\u2500 transcript.json # Full transcript with segments\n\u2502 \u251c\u2500\u2500 transcript.txt # Plain text\n\u2502 \u2514\u2500\u2500 transcript.srt # Subtitles\n\u251c\u2500\u2500 frames/\n\u2502 \u251c\u2500\u2500 frame_0000.jpg # Extracted video frames\n\u2502 \u251c\u2500\u2500 frame_0001.jpg\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 diagrams/\n\u2502 \u251c\u2500\u2500 diagram_0.jpg # Original frame\n\u2502 \u251c\u2500\u2500 diagram_0.mermaid # Mermaid source\n\u2502 \u251c\u2500\u2500 diagram_0.svg # Vector rendering\n\u2502 \u251c\u2500\u2500 diagram_0.png # Raster rendering\n\u2502 \u251c\u2500\u2500 diagram_0.json # Analysis data\n\u2502 \u251c\u2500\u2500 chart_0.svg # Reproduced chart (SVG)\n\u2502 \u251c\u2500\u2500 chart_0.png # Reproduced chart (PNG)\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 captures/\n\u2502 \u251c\u2500\u2500 capture_0.jpg # Medium-confidence screenshots\n\u2502 \u251c\u2500\u2500 capture_0.json # Caption and metadata\n\u2502 \u2514\u2500\u2500 ...\n\u2514\u2500\u2500 results/\n \u251c\u2500\u2500 analysis.md # Markdown report\n \u251c\u2500\u2500 analysis.html # HTML report\n \u251c\u2500\u2500 analysis.pdf # PDF report (if planopticon[pdf] installed)\n \u251c\u2500\u2500 knowledge_graph.db # Knowledge graph (SQLite, primary)\n \u251c\u2500\u2500 knowledge_graph.json # Knowledge graph (JSON export)\n \u251c\u2500\u2500 key_points.json # Extracted key points\n \u2514\u2500\u2500 action_items.json # Action items\n</code></pre>"},{"location":"guide/output-formats/#controlling-output-format","title":"Controlling Output Format","text":"<p>Use the <code>--output-format</code> flag with <code>planopticon analyze</code> to control how results are presented:</p> Value Behavior <code>default</code> Writes all output files to disk and prints a usage summary to stdout. <code>json</code> Writes all output files to disk and also emits the complete <code>VideoManifest</code> as structured JSON to stdout. Useful for piping into other tools or CI/CD pipelines. <pre><code># Standard output (files + console summary)\nplanopticon analyze -i video.mp4 -o ./output\n\n# JSON manifest to stdout (for scripting)\nplanopticon analyze -i video.mp4 -o ./output --output-format json\n</code></pre>"},{"location":"guide/planning-agent/","title":"Planning Agent","text":"<p>The Planning Agent is PlanOpticon's AI-powered system for synthesizing knowledge graph content into structured planning artifacts. It takes extracted entities and relationships from video analyses, document ingestions, and other sources, then uses LLM reasoning to produce project plans, PRDs, roadmaps, task breakdowns, GitHub issues, and more.</p>"},{"location":"guide/planning-agent/#how-it-works","title":"How It Works","text":"<p>The Planning Agent operates through a three-stage pipeline:</p>"},{"location":"guide/planning-agent/#1-context-assembly","title":"1. Context Assembly","text":"<p>The agent gathers context from all available sources:</p> <ul> <li>Knowledge graph -- entity counts, types, relationships, and planning entities from the loaded KG</li> <li>Query engine -- used to pull stats, entity lists, and relationship data for prompt construction</li> <li>Provider manager -- the configured LLM provider used for generation</li> <li>Prior artifacts -- any artifacts already generated in the session (skills can chain off each other)</li> <li>Conversation history -- accumulated chat messages when running in interactive mode</li> </ul> <p>This context is bundled into an <code>AgentContext</code> dataclass that is shared across all skills.</p>"},{"location":"guide/planning-agent/#2-skill-selection","title":"2. Skill Selection","text":"<p>When the agent receives a user request, it determines which skills to run:</p> <p>LLM-driven planning (with provider). The agent constructs a prompt that includes the knowledge base summary, all available skill names and descriptions, and the user's request. The LLM returns a JSON array of skill names to execute in order, along with any parameters. For example, given \"Create a project plan and break it into tasks,\" the LLM might select <code>[\"project_plan\", \"task_breakdown\"]</code>.</p> <p>Keyword fallback (without provider). If no LLM provider is available, the agent falls back to simple keyword matching. It splits each skill name on underscores and checks whether any of those words appear in the user's request. For example, the request \"generate a roadmap\" would match the <code>roadmap</code> skill because \"roadmap\" appears in both the request and the skill name.</p>"},{"location":"guide/planning-agent/#3-execution","title":"3. Execution","text":"<p>Selected skills are executed sequentially. Each skill:</p> <ol> <li>Checks <code>can_execute()</code> to verify the required context is available (by default, both a knowledge graph and an LLM provider must be present)</li> <li>Pulls relevant data from the knowledge graph via the query engine</li> <li>Constructs a detailed prompt for the LLM with extracted context</li> <li>Calls the LLM and parses the response</li> <li>Returns an <code>Artifact</code> object containing the generated content</li> </ol> <p>Each artifact is appended to <code>context.artifacts</code>, making it available to subsequent skills. This enables chaining -- for example, <code>task_breakdown</code> can feed into <code>github_issues</code>.</p>"},{"location":"guide/planning-agent/#agentcontext","title":"AgentContext","text":"<p>The <code>AgentContext</code> dataclass is the shared state object that connects all components of the planning agent system.</p> <pre><code>@dataclass\nclass AgentContext:\n knowledge_graph: Any = None # KnowledgeGraph instance\n query_engine: Any = None # GraphQueryEngine instance\n provider_manager: Any = None # ProviderManager instance\n planning_entities: List[Any] = field(default_factory=list)\n user_requirements: Dict[str, Any] = field(default_factory=dict)\n conversation_history: List[Dict[str, str]] = field(default_factory=list)\n artifacts: List[Artifact] = field(default_factory=list)\n config: Dict[str, Any] = field(default_factory=dict)\n</code></pre> Field Purpose <code>knowledge_graph</code> The loaded <code>KnowledgeGraph</code> instance; provides access to entities, relationships, and graph operations <code>query_engine</code> A <code>GraphQueryEngine</code> for running structured queries (stats, entities, neighbors, relationships) <code>provider_manager</code> The <code>ProviderManager</code> that handles LLM API calls across providers <code>planning_entities</code> Entities classified into the planning taxonomy (goals, requirements, risks, etc.) <code>user_requirements</code> Structured requirements gathered from the <code>requirements_chat</code> skill <code>conversation_history</code> Accumulated chat messages for interactive sessions <code>artifacts</code> All artifacts generated during the session, enabling skill chaining <code>config</code> Arbitrary configuration overrides"},{"location":"guide/planning-agent/#artifacts","title":"Artifacts","text":"<p>Every skill returns an <code>Artifact</code> dataclass:</p> <pre><code>@dataclass\nclass Artifact:\n name: str # Human-readable name (e.g., \"Project Plan\")\n content: str # The generated content (markdown, JSON, etc.)\n artifact_type: str # Type identifier: \"project_plan\", \"prd\", \"roadmap\", etc.\n format: str = \"markdown\" # Content format: \"markdown\", \"json\", \"mermaid\"\n metadata: Dict[str, Any] = field(default_factory=dict)\n</code></pre> <p>Artifacts are the currency of the agent system. They can be:</p> <ul> <li>Displayed directly in the Companion REPL</li> <li>Exported to disk via the <code>artifact_export</code> skill</li> <li>Pushed to external tools via the <code>cli_adapter</code> skill</li> <li>Chained into other skills (e.g., task breakdown feeds into GitHub issues)</li> </ul>"},{"location":"guide/planning-agent/#skills-reference","title":"Skills Reference","text":"<p>The agent ships with 11 built-in skills. Each skill is a class that extends <code>Skill</code> and self-registers at import time via <code>register_skill()</code>.</p>"},{"location":"guide/planning-agent/#project_plan","title":"project_plan","text":"<p>Description: Generate a structured project plan from knowledge graph.</p> <p>Pulls the full knowledge graph context (stats, entities, relationships, and planning entities grouped by type) and asks the LLM to produce a comprehensive project plan with:</p> <ol> <li>Executive Summary</li> <li>Goals and Objectives</li> <li>Scope</li> <li>Phases and Milestones</li> <li>Resource Requirements</li> <li>Risks and Mitigations</li> <li>Success Criteria</li> </ol> <p>Artifact type: <code>project_plan</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#prd","title":"prd","text":"<p>Description: Generate a product requirements document (PRD) / feature spec.</p> <p>Filters planning entities to those of type <code>requirement</code>, <code>feature</code>, and <code>constraint</code>, then asks the LLM to generate a PRD with:</p> <ol> <li>Problem Statement</li> <li>User Stories</li> <li>Functional Requirements</li> <li>Non-Functional Requirements</li> <li>Acceptance Criteria</li> <li>Out of Scope</li> </ol> <p>If no pre-filtered entities match, the LLM derives requirements from the full knowledge graph context.</p> <p>Artifact type: <code>prd</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#roadmap","title":"roadmap","text":"<p>Description: Generate a product/project roadmap.</p> <p>Focuses on planning entities of type <code>milestone</code>, <code>feature</code>, and <code>dependency</code>. Asks the LLM to produce a roadmap with:</p> <ol> <li>Vision and Strategy</li> <li>Phases (with timeline estimates)</li> <li>Key Dependencies</li> <li>A Mermaid Gantt chart summarizing the timeline</li> </ol> <p>Artifact type: <code>roadmap</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#task_breakdown","title":"task_breakdown","text":"<p>Description: Break down goals into tasks with dependencies.</p> <p>Focuses on planning entities of type <code>goal</code>, <code>feature</code>, and <code>milestone</code>. Returns a JSON array of task objects, each containing:</p> Field Type Description <code>id</code> string Task identifier (e.g., \"T1\", \"T2\") <code>title</code> string Short task title <code>description</code> string Detailed description <code>depends_on</code> list IDs of prerequisite tasks <code>priority</code> string <code>high</code>, <code>medium</code>, or <code>low</code> <code>estimate</code> string Effort estimate (e.g., \"2d\", \"1w\") <code>assignee_role</code> string Role needed to perform the task <p>Artifact type: <code>task_list</code> | Format: json</p>"},{"location":"guide/planning-agent/#github_issues","title":"github_issues","text":"<p>Description: Generate GitHub issues from task breakdown.</p> <p>Converts tasks into GitHub-ready issue objects. If a <code>task_list</code> artifact exists in the context, it is used as input. Otherwise, minimal issues are generated from the planning entities directly.</p> <p>Each issue includes a formatted body with description, priority, estimate, and dependencies, plus labels derived from the task priority.</p> <p>The skill also provides a <code>push_to_github(issues_json, repo)</code> function that shells out to the <code>gh</code> CLI to create actual issues. This is used by the <code>cli_adapter</code> skill.</p> <p>Artifact type: <code>issues</code> | Format: json</p>"},{"location":"guide/planning-agent/#requirements_chat","title":"requirements_chat","text":"<p>Description: Interactive requirements gathering via guided questions.</p> <p>Generates a structured requirements questionnaire based on the knowledge graph context. The questionnaire contains 8-12 targeted questions, each with:</p> Field Type Description <code>id</code> string Question identifier (e.g., \"Q1\") <code>category</code> string <code>goals</code>, <code>constraints</code>, <code>priorities</code>, or <code>scope</code> <code>question</code> string The question text <code>context</code> string Why this question matters <p>The skill also provides a <code>gather_requirements(context, answers)</code> method that takes the completed Q&A and synthesizes structured requirements (goals, constraints, priorities, scope).</p> <p>Artifact type: <code>requirements</code> | Format: json</p>"},{"location":"guide/planning-agent/#doc_generator","title":"doc_generator","text":"<p>Description: Generate technical documentation, ADRs, or meeting notes.</p> <p>Supports three document types, selected via the <code>doc_type</code> parameter:</p> <code>doc_type</code> Output Structure <code>technical_doc</code> (default) Overview, Architecture, Components and Interfaces, Data Flow, Deployment and Configuration, API Reference <code>adr</code> Title, Status (Proposed), Context, Decision, Consequences, Alternatives Considered <code>meeting_notes</code> Meeting Summary, Key Discussion Points, Decisions Made, Action Items (with owners), Open Questions, Next Steps <p>Artifact type: <code>document</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#artifact_export","title":"artifact_export","text":"<p>Description: Export artifacts in agent-ready formats.</p> <p>Writes all artifacts accumulated in the context to a directory structure. Each artifact is written to a file based on its type:</p> Artifact Type Filename <code>project_plan</code> <code>project_plan.md</code> <code>prd</code> <code>prd.md</code> <code>roadmap</code> <code>roadmap.md</code> <code>task_list</code> <code>tasks.json</code> <code>issues</code> <code>issues.json</code> <code>requirements</code> <code>requirements.json</code> <code>document</code> <code>docs/<name>.md</code> <p>A <code>manifest.json</code> is written alongside, listing all exported files with their names, types, and formats.</p> <p>Artifact type: <code>export_manifest</code> | Format: json</p> <p>Accepts an <code>output_dir</code> parameter (defaults to <code>plan/</code>).</p>"},{"location":"guide/planning-agent/#cli_adapter","title":"cli_adapter","text":"<p>Description: Push artifacts to external tools via their CLIs.</p> <p>Converts artifacts into CLI commands for external project management tools. Supported tools:</p> Tool CLI Example Command <code>github</code> <code>gh</code> <code>gh issue create --title \"...\" --body \"...\" --label \"...\"</code> <code>jira</code> <code>jira</code> <code>jira issue create --summary \"...\" --description \"...\"</code> <code>linear</code> <code>linear</code> <code>linear issue create --title \"...\" --description \"...\"</code> <p>The skill checks whether the target CLI is available on the system and includes that status in the output. Commands are generated in dry-run mode by default.</p> <p>Artifact type: <code>cli_commands</code> | Format: json</p>"},{"location":"guide/planning-agent/#notes_export","title":"notes_export","text":"<p>Description: Export knowledge graph as structured notes (Obsidian, Notion).</p> <p>Exports the entire knowledge graph as a collection of markdown files optimized for a specific note-taking platform. Accepts a <code>format</code> parameter:</p> <p>Obsidian format creates:</p> <ul> <li>One <code>.md</code> file per entity with YAML frontmatter, tags, and <code>[[wiki-links]]</code></li> <li>An <code>_Index.md</code> Map of Content grouping entities by type</li> <li>Tag pages for each entity type</li> <li>Artifact notes for any generated artifacts</li> </ul> <p>Notion format creates:</p> <ul> <li>One <code>.md</code> file per entity with Notion-style callout blocks and relationship tables</li> <li>An <code>entities_database.csv</code> for bulk import into a Notion database</li> <li>An <code>Overview.md</code> page with stats and entity listings</li> <li>Artifact pages</li> </ul> <p>Artifact type: <code>notes_export</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#wiki_generator","title":"wiki_generator","text":"<p>Description: Generate a GitHub wiki from knowledge graph and artifacts.</p> <p>Generates a complete GitHub wiki structure as a dictionary of page names to markdown content. Creates:</p> <ul> <li>Home page with entity type counts and links</li> <li>_Sidebar navigation with entity types and artifacts</li> <li>Type index pages with tables of entities per type</li> <li>Individual entity pages with descriptions, outgoing/incoming relationships, and source occurrences</li> <li>Artifact pages for any generated planning artifacts</li> </ul> <p>The skill also provides standalone functions <code>write_wiki(pages, output_dir)</code> to write pages to disk and <code>push_wiki(wiki_dir, repo)</code> to push directly to a GitHub wiki repository.</p> <p>Artifact type: <code>wiki</code> | Format: markdown</p>"},{"location":"guide/planning-agent/#cli-usage","title":"CLI Usage","text":""},{"location":"guide/planning-agent/#one-shot-execution","title":"One-shot execution","text":"<p>Run the agent with a request string. The agent selects and executes appropriate skills automatically.</p> <pre><code># Generate a project plan\nplanopticon agent \"Create a project plan\" --kb ./results\n\n# Generate a PRD\nplanopticon agent \"Write a PRD for the authentication system\" --kb ./results\n\n# Break down into tasks\nplanopticon agent \"Break this into tasks and estimate effort\" --kb ./results\n</code></pre>"},{"location":"guide/planning-agent/#export-artifacts-to-disk","title":"Export artifacts to disk","text":"<p>Use <code>--export</code> to write generated artifacts to a directory:</p> <pre><code>planopticon agent \"Create a full project plan with tasks\" --kb ./results --export ./output\n</code></pre>"},{"location":"guide/planning-agent/#interactive-mode","title":"Interactive mode","text":"<p>Use <code>-I</code> for a multi-turn session where you can issue multiple requests:</p> <pre><code>planopticon agent -I --kb ./results\n</code></pre> <p>In interactive mode, the agent supports:</p> <ul> <li>Free-text requests (executed via LLM skill selection)</li> <li><code>/plan</code> -- shortcut to generate a project plan</li> <li><code>/skills</code> -- list available skills</li> <li><code>quit</code>, <code>exit</code>, <code>q</code> -- end the session</li> </ul>"},{"location":"guide/planning-agent/#provider-and-model-options","title":"Provider and model options","text":"<pre><code># Use a specific provider\nplanopticon agent \"Create a roadmap\" --kb ./results -p anthropic\n\n# Use a specific model\nplanopticon agent \"Generate a PRD\" --kb ./results --chat-model gpt-4o\n</code></pre>"},{"location":"guide/planning-agent/#auto-discovery","title":"Auto-discovery","text":"<p>If <code>--kb</code> is not specified, the agent uses <code>KBContext.auto_discover()</code> to find knowledge graphs in the workspace.</p>"},{"location":"guide/planning-agent/#using-skills-from-the-companion-repl","title":"Using Skills from the Companion REPL","text":"<p>The Companion REPL provides direct access to agent skills through slash commands. See the Companion guide for full details.</p> Companion Command Skill Executed <code>/plan</code> <code>project_plan</code> <code>/prd</code> <code>prd</code> <code>/tasks</code> <code>task_breakdown</code> <code>/run SKILL_NAME</code> Any registered skill by name <p>When executed from the Companion, skills use the same <code>AgentContext</code> that powers the chat mode. This means:</p> <ul> <li>The knowledge graph loaded at startup is automatically available</li> <li>The active LLM provider (set via <code>/provider</code> or <code>/model</code>) is used for generation</li> <li>Generated artifacts accumulate across the session, enabling chaining</li> </ul>"},{"location":"guide/planning-agent/#example-workflows","title":"Example Workflows","text":""},{"location":"guide/planning-agent/#from-video-to-project-plan","title":"From video to project plan","text":"<pre><code># 1. Analyze a video\nplanopticon analyze -i sprint-review.mp4 -o results/\n\n# 2. Launch the agent with the results\nplanopticon agent \"Create a comprehensive project plan with tasks and a roadmap\" \\\n --kb results/ --export plan/\n\n# 3. Review the generated artifacts\nls plan/\n# project_plan.md roadmap.md tasks.json manifest.json\n</code></pre>"},{"location":"guide/planning-agent/#interactive-planning-session","title":"Interactive planning session","text":"<pre><code>$ planopticon companion --kb ./results\n\nplanopticon> /status\nWorkspace status:\n KG: knowledge_graph.db (58 entities, 124 relationships)\n ...\n\nplanopticon> What are the main goals discussed?\nBased on the knowledge graph, the main goals are...\n\nplanopticon> /plan\n--- Project Plan (project_plan) ---\n...\n\nplanopticon> /tasks\n--- Task Breakdown (task_list) ---\n...\n\nplanopticon> /run github_issues\n--- GitHub Issues (issues) ---\n[\n {\"title\": \"Set up authentication service\", ...},\n ...\n]\n\nplanopticon> /run artifact_export\n--- Export Manifest (export_manifest) ---\n{\n \"artifact_count\": 3,\n \"output_dir\": \"plan\",\n \"files\": [...]\n}\n</code></pre>"},{"location":"guide/planning-agent/#skill-chaining","title":"Skill chaining","text":"<p>Skills that produce artifacts make them available to subsequent skills automatically:</p> <ol> <li><code>/tasks</code> generates a <code>task_list</code> artifact</li> <li><code>/run github_issues</code> detects the existing <code>task_list</code> artifact and converts its tasks into GitHub issues</li> <li><code>/run cli_adapter</code> takes the most recent artifact and generates <code>gh issue create</code> commands</li> <li><code>/run artifact_export</code> writes all accumulated artifacts to disk with a manifest</li> </ol> <p>This chaining works both in the Companion REPL and in one-shot agent execution, since the <code>AgentContext.artifacts</code> list persists for the duration of the session.</p>"},{"location":"guide/single-video/","title":"Single Video Analysis","text":""},{"location":"guide/single-video/#basic-usage","title":"Basic usage","text":"<pre><code>planopticon analyze -i recording.mp4 -o ./output\n</code></pre>"},{"location":"guide/single-video/#what-happens","title":"What happens","text":"<p>The pipeline runs these steps in order:</p> <ol> <li>Frame extraction -- Samples frames using change detection for transitions plus periodic capture (every 30s) for slow-evolving content like document scrolling</li> <li>People frame filtering -- OpenCV face detection automatically removes webcam/video conference frames, keeping only shared content (slides, documents, screen shares)</li> <li>Audio extraction -- Extracts audio track to WAV</li> <li>Transcription -- Sends audio to speech-to-text (Whisper or Gemini). If <code>--speakers</code> is provided, speaker diarization hints are passed to the provider.</li> <li>Diagram detection -- Vision model classifies each frame as diagram/chart/whiteboard/screenshot/none</li> <li>Diagram analysis -- High-confidence diagrams get full extraction (description, text, mermaid, chart data)</li> <li>Screengrab fallback -- Medium-confidence frames are saved as captioned screenshots</li> <li>Knowledge graph -- Extracts entities and relationships from transcript + diagrams, stored in both <code>knowledge_graph.db</code> (SQLite, primary) and <code>knowledge_graph.json</code> (export)</li> <li>Key points -- LLM extracts main points and topics</li> <li>Action items -- LLM finds tasks, commitments, and follow-ups</li> <li>Reports -- Generates markdown, HTML, and PDF</li> <li>Export -- Renders mermaid diagrams to SVG/PNG, reproduces charts</li> </ol> <p>After analysis, you can optionally run planning taxonomy classification on the knowledge graph to categorize entities for use with the planning agent:</p> <pre><code>planopticon kg classify results/knowledge_graph.db\n</code></pre>"},{"location":"guide/single-video/#processing-depth","title":"Processing depth","text":""},{"location":"guide/single-video/#basic","title":"<code>basic</code>","text":"<ul> <li>Transcription only</li> <li>Key points and action items</li> <li>No diagram extraction</li> </ul>"},{"location":"guide/single-video/#standard-default","title":"<code>standard</code> (default)","text":"<ul> <li>Everything in basic</li> <li>Diagram extraction (up to 10 frames, evenly sampled)</li> <li>Knowledge graph</li> <li>Full report generation</li> </ul>"},{"location":"guide/single-video/#comprehensive","title":"<code>comprehensive</code>","text":"<ul> <li>Everything in standard</li> <li>More frames analyzed (up to 20)</li> <li>Deeper analysis</li> </ul>"},{"location":"guide/single-video/#command-line-options","title":"Command-line options","text":""},{"location":"guide/single-video/#provider-and-model-selection","title":"Provider and model selection","text":"<pre><code># Use a specific provider\nplanopticon analyze -i video.mp4 -o ./output --provider anthropic\n\n# Override vision and chat models separately\nplanopticon analyze -i video.mp4 -o ./output --vision-model gpt-4o --chat-model claude-sonnet-4-20250514\n</code></pre>"},{"location":"guide/single-video/#speaker-diarization-hints","title":"Speaker diarization hints","text":"<p>Use <code>--speakers</code> to provide speaker names as comma-separated hints. These are passed to the transcription provider to improve speaker identification in the transcript segments.</p> <pre><code>planopticon analyze -i video.mp4 -o ./output --speakers \"Alice,Bob,Carol\"\n</code></pre>"},{"location":"guide/single-video/#custom-prompt-templates","title":"Custom prompt templates","text":"<p>Use <code>--templates-dir</code> to point to a directory of custom <code>.txt</code> prompt template files. These override the built-in prompts used for diagram analysis, key point extraction, action item extraction, and other LLM-driven steps.</p> <pre><code>planopticon analyze -i video.mp4 -o ./output --templates-dir ./my-prompts\n</code></pre> <p>Template files should be named to match the built-in template names (e.g., <code>key_points.txt</code>, <code>action_items.txt</code>). See the <code>video_processor/utils/prompt_templates.py</code> module for the full list of template names.</p>"},{"location":"guide/single-video/#output-format","title":"Output format","text":"<p>Use <code>--output-format json</code> to emit the complete <code>VideoManifest</code> as structured JSON to stdout, in addition to writing all output files to disk. This is useful for scripting, CI/CD integration, or piping results into other tools.</p> <pre><code># Standard output (files + console summary)\nplanopticon analyze -i video.mp4 -o ./output\n\n# JSON manifest to stdout\nplanopticon analyze -i video.mp4 -o ./output --output-format json\n</code></pre>"},{"location":"guide/single-video/#frame-extraction-tuning","title":"Frame extraction tuning","text":"<pre><code># Adjust sampling rate (frames per second to consider)\nplanopticon analyze -i video.mp4 -o ./output --sampling-rate 1.0\n\n# Adjust change detection threshold (lower = more sensitive)\nplanopticon analyze -i video.mp4 -o ./output --change-threshold 0.10\n\n# Adjust periodic capture interval\nplanopticon analyze -i video.mp4 -o ./output --periodic-capture 60\n\n# Enable GPU acceleration for frame extraction\nplanopticon analyze -i video.mp4 -o ./output --use-gpu\n</code></pre>"},{"location":"guide/single-video/#output-structure","title":"Output structure","text":"<p>Every run produces a standardized directory structure:</p> <pre><code>output/\n\u251c\u2500\u2500 manifest.json # Run manifest (source of truth)\n\u251c\u2500\u2500 transcript/\n\u2502 \u251c\u2500\u2500 transcript.json # Full transcript with segments + speakers\n\u2502 \u251c\u2500\u2500 transcript.txt # Plain text\n\u2502 \u2514\u2500\u2500 transcript.srt # Subtitles\n\u251c\u2500\u2500 frames/\n\u2502 \u251c\u2500\u2500 frame_0000.jpg\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 diagrams/\n\u2502 \u251c\u2500\u2500 diagram_0.jpg # Original frame\n\u2502 \u251c\u2500\u2500 diagram_0.mermaid # Mermaid source\n\u2502 \u251c\u2500\u2500 diagram_0.svg # Vector rendering\n\u2502 \u251c\u2500\u2500 diagram_0.png # Raster rendering\n\u2502 \u251c\u2500\u2500 diagram_0.json # Analysis data\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500\u2500 captures/\n\u2502 \u251c\u2500\u2500 capture_0.jpg # Medium-confidence screenshots\n\u2502 \u251c\u2500\u2500 capture_0.json\n\u2502 \u2514\u2500\u2500 ...\n\u2514\u2500\u2500 results/\n \u251c\u2500\u2500 analysis.md # Markdown report\n \u251c\u2500\u2500 analysis.html # HTML report\n \u251c\u2500\u2500 analysis.pdf # PDF (if planopticon[pdf] installed)\n \u251c\u2500\u2500 knowledge_graph.db # Knowledge graph (SQLite, primary)\n \u251c\u2500\u2500 knowledge_graph.json # Knowledge graph (JSON export)\n \u251c\u2500\u2500 key_points.json # Extracted key points\n \u2514\u2500\u2500 action_items.json # Action items\n</code></pre>"},{"location":"guide/single-video/#output-manifest","title":"Output manifest","text":"<p>Every run produces a <code>manifest.json</code> that is the single source of truth:</p> <pre><code>{\n \"version\": \"1.0\",\n \"video\": {\n \"title\": \"Analysis of recording\",\n \"source_path\": \"/path/to/recording.mp4\",\n \"duration_seconds\": 3600.0\n },\n \"stats\": {\n \"duration_seconds\": 45.2,\n \"frames_extracted\": 42,\n \"people_frames_filtered\": 11,\n \"diagrams_detected\": 3,\n \"screen_captures\": 5,\n \"models_used\": {\n \"vision\": \"gpt-4o\",\n \"chat\": \"gpt-4o\"\n }\n },\n \"transcript_json\": \"transcript/transcript.json\",\n \"transcript_txt\": \"transcript/transcript.txt\",\n \"transcript_srt\": \"transcript/transcript.srt\",\n \"analysis_md\": \"results/analysis.md\",\n \"knowledge_graph_json\": \"results/knowledge_graph.json\",\n \"knowledge_graph_db\": \"results/knowledge_graph.db\",\n \"key_points_json\": \"results/key_points.json\",\n \"action_items_json\": \"results/action_items.json\",\n \"key_points\": [...],\n \"action_items\": [...],\n \"diagrams\": [...],\n \"screen_captures\": [...]\n}\n</code></pre>"},{"location":"guide/single-video/#checkpoint-and-resume","title":"Checkpoint and resume","text":"<p>The pipeline supports checkpoint/resume. If a step's output files already exist on disk, that step is skipped on re-run. This means you can safely re-run an interrupted analysis and it will pick up where it left off:</p> <pre><code># First run (interrupted at step 6)\nplanopticon analyze -i video.mp4 -o ./output\n\n# Second run (resumes from step 6)\nplanopticon analyze -i video.mp4 -o ./output\n</code></pre>"},{"location":"guide/single-video/#using-results-after-analysis","title":"Using results after analysis","text":""},{"location":"guide/single-video/#query-the-knowledge-graph","title":"Query the knowledge graph","text":"<p>After analysis completes, you can query the knowledge graph directly:</p> <pre><code># Show graph stats\nplanopticon query --db results/knowledge_graph.db\n\n# List entities by type\nplanopticon query --db results/knowledge_graph.db \"entities --type technology\"\n\n# Find neighbors of an entity\nplanopticon query --db results/knowledge_graph.db \"neighbors Kubernetes\"\n\n# Ask natural language questions (requires API key)\nplanopticon query --db results/knowledge_graph.db \"What technologies were discussed?\"\n</code></pre>"},{"location":"guide/single-video/#classify-entities-for-planning","title":"Classify entities for planning","text":"<p>Run taxonomy classification to categorize entities into planning types (goal, milestone, risk, dependency, etc.):</p> <pre><code>planopticon kg classify results/knowledge_graph.db\nplanopticon kg classify results/knowledge_graph.db --format json\n</code></pre>"},{"location":"guide/single-video/#export-to-other-formats","title":"Export to other formats","text":"<pre><code># Generate markdown documents\nplanopticon export markdown results/knowledge_graph.db -o ./docs\n\n# Export as Obsidian vault\nplanopticon export obsidian results/knowledge_graph.db -o ./vault\n\n# Export as PlanOpticonExchange\nplanopticon export exchange results/knowledge_graph.db -o exchange.json\n\n# Generate GitHub wiki\nplanopticon wiki generate results/knowledge_graph.db -o ./wiki\n</code></pre>"},{"location":"guide/single-video/#use-with-the-planning-agent","title":"Use with the planning agent","text":"<p>The planning agent can consume the knowledge graph to generate project plans, PRDs, roadmaps, and other planning artifacts:</p> <pre><code>planopticon agent --db results/knowledge_graph.db\n</code></pre>"}]}