šŸ” Code Extractor

function main_v68

Maturity: 43

Async entry point for an E-Ink LLM Assistant that processes handwritten/drawn content using AI vision models, supporting local files, reMarkable Cloud, and OneDrive integration.

File:
/tf/active/vicechatdev/e-ink-llm/main.py
Lines:
146 - 643
Complexity:
complex

Purpose

This is the main CLI application entry point that orchestrates an AI-powered document processing system designed for e-ink devices. It supports multiple modes: single file processing, file watching, reMarkable Cloud integration, OneDrive integration, and mixed cloud modes. The system processes handwritten notes, drawings, and PDFs using OpenAI's GPT-4 Vision API, maintains conversation history, generates responses optimized for e-ink displays, and can sync with cloud storage services. It includes features like conversation management, timeline generation, multi-page PDF processing, annotation detection, and hybrid text/graphics generation.

Source Code

async def main():
    parser = argparse.ArgumentParser(
        description="E-Ink LLM Assistant - Process handwritten/drawn content with AI",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Start file watcher (default mode)
  python main.py --watch-folder ./documents

  # Process a single file
  python main.py --file drawing.pdf

  # Start watcher with custom API key
  python main.py --api-key sk-... --watch-folder ./input

  # Continue existing conversation
  python main.py --conversation-id conv_20250731_143022_a8f9c2d1 --file new_question.pdf

  # Use verbose formatting instead of compact
  python main.py --verbose-mode --file document.pdf

  # List active conversations
  python main.py --list-conversations

Environment Variables:
  OPENAI_API_KEY    OpenAI API key for GPT-4 Vision models

Supported File Types:
  PDF, JPG, JPEG, PNG, GIF, BMP, TIFF, WEBP

Output:
  - Response PDFs: RESPONSE_[conv_id]_ex[num]_[filename].pdf
  - Error reports: ERROR_[conv_id]_ex[num]_[filename].pdf
  - Activity logs: eink_llm.log
  - Session database: eink_sessions.db
        """
    )
    
    # Mode selection
    mode_group = parser.add_mutually_exclusive_group()
    mode_group.add_argument(
        '--file', '-f',
        type=str,
        help='Process a single file instead of watching a folder'
    )
    mode_group.add_argument(
        '--watch-folder', '-w',
        type=str,
        help='Folder to watch for new files (default: ./watch)'
    )
    mode_group.add_argument(
        '--remarkable-document-id',
        type=str,
        help='Process a single document from reMarkable Cloud by ID'
    )
    
    # Operation mode
    parser.add_argument(
        '--mode',
        choices=['local', 'remarkable', 'onedrive', 'both', 'mixed'],
        default='local',
        help='Processing mode: local file watching, reMarkable Cloud, OneDrive, both, or mixed (mixed = monitors both OneDrive and reMarkable for input, outputs to OneDrive) (default: local)'
    )
    
    # Configuration options
    parser.add_argument(
        '--api-key',
        type=str,
        help='OpenAI API key (can also use OPENAI_API_KEY environment variable)'
    )
    parser.add_argument(
        '--no-existing',
        action='store_true',
        help='Skip processing existing files when starting watcher'
    )
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='Enable verbose output'
    )
    parser.add_argument(
        '--conversation-id',
        type=str,
        help='Continue existing conversation by ID (default: create new)'
    )
    parser.add_argument(
        '--compact-mode',
        action='store_true',
        default=True,
        help='Use compact response formatting for e-ink optimization (default: enabled)'
    )
    parser.add_argument(
        '--verbose-mode',
        action='store_true',
        help='Use verbose response formatting (disables compact mode)'
    )
    parser.add_argument(
        '--no-auto-detect',
        action='store_true',
        help='Disable automatic session detection from PDF metadata/content'
    )
    parser.add_argument(
        '--no-multi-page',
        action='store_true',
        help='Disable multi-page PDF processing (process only first page)'
    )
    parser.add_argument(
        '--max-pages',
        type=int,
        default=50,
        help='Maximum pages to process in multi-page PDFs (default: 50)'
    )
    parser.add_argument(
        '--no-editing-workflow',
        action='store_true',
        help='Disable annotation detection and text editing workflow'
    )
    parser.add_argument(
        '--enable-hybrid-mode',
        action='store_true',
        default=True,
        help='Enable hybrid mode with text and graphics generation (default: enabled)'
    )
    parser.add_argument(
        '--no-hybrid-mode',
        action='store_true',
        help='Disable hybrid mode, use text-only responses'
    )
    parser.add_argument(
        '--list-conversations',
        action='store_true',
        help='List active conversations and exit'
    )
    parser.add_argument(
        '--generate-timeline',
        type=str,
        help='Generate conversation timeline PDF for specified conversation ID'
    )
    
    # reMarkable Cloud specific options
    remarkable_group = parser.add_argument_group('reMarkable Cloud Options')
    remarkable_group.add_argument(
        '--remarkable-config',
        type=str,
        help='Path to JSON config file for reMarkable Cloud settings'
    )
    remarkable_group.add_argument(
        '--remarkable-watch-folder',
        type=str,
        default='/E-Ink LLM Input',
        help='Folder path in reMarkable Cloud to watch for input files (default: /E-Ink LLM Input)'
    )
    remarkable_group.add_argument(
        '--remarkable-output-folder',
        type=str,
        default='/E-Ink LLM Output',
        help='Folder path in reMarkable Cloud to upload responses (default: /E-Ink LLM Output)'
    )
    remarkable_group.add_argument(
        '--remarkable-one-time-code',
        type=str,
        help='One-time code from reMarkable account for initial authentication'
    )
    remarkable_group.add_argument(
        '--remarkable-poll-interval',
        type=int,
        default=60,
        help='Seconds between checks for new files in reMarkable Cloud (default: 60)'
    )
    
    # OneDrive specific options
    onedrive_group = parser.add_argument_group('OneDrive Options')
    onedrive_group.add_argument(
        '--onedrive-config',
        type=str,
        help='Path to JSON config file for OneDrive settings'
    )
    onedrive_group.add_argument(
        '--onedrive-watch-folder',
        type=str,
        default='/E-Ink LLM Input',
        help='Folder path in OneDrive to watch for input files (default: /E-Ink LLM Input)'
    )
    onedrive_group.add_argument(
        '--onedrive-output-folder',
        type=str,
        default='/E-Ink LLM Output',
        help='Folder path in OneDrive to upload responses (default: /E-Ink LLM Output)'
    )
    onedrive_group.add_argument(
        '--onedrive-poll-interval',
        type=int,
        default=60,
        help='Seconds between checks for new files in OneDrive (default: 60)'
    )
    onedrive_group.add_argument(
        '--onedrive-client-id',
        type=str,
        help='Azure App Registration client ID for OneDrive access'
    )
    
    args = parser.parse_args()
    
    # Handle timeline generation
    if args.generate_timeline:
        from conversation_timeline import ConversationTimelineGenerator
        
        session_manager = SessionManager()
        timeline_generator = ConversationTimelineGenerator()
        
        # Check if conversation exists
        conversation = session_manager.get_conversation(args.generate_timeline)
        if not conversation:
            print(f"āŒ Error: Conversation '{args.generate_timeline}' not found.")
            sys.exit(1)
        
        print(f"šŸ“Š Generating timeline for conversation: {args.generate_timeline}")
        timeline_path = await timeline_generator.generate_timeline_pdf(
            conversation_id=args.generate_timeline,
            session_manager=session_manager
        )
        
        if timeline_path:
            print(f"āœ… Timeline generated successfully: {timeline_path}")
        else:
            print("āŒ Error: Failed to generate timeline PDF")
            sys.exit(1)
        
        sys.exit(0)
    
    # Handle conversation listing
    if args.list_conversations:
        session_manager = SessionManager()
        conversations = session_manager.list_active_conversations()
        
        if conversations:
            print("šŸ—‚ļø  Active Conversations:")
            print("=" * 70)
            for conv in conversations:
                print(f"šŸ†” {conv['conversation_id']}")
                print(f"   šŸ“… Created: {conv['created_at']}")
                print(f"   šŸ• Last activity: {conv['last_activity']}")
                print(f"   šŸ’¬ Exchanges: {conv['total_exchanges']}")
                if conv['user_id']:
                    print(f"   šŸ‘¤ User: {conv['user_id']}")
                print()
        else:
            print("šŸ“ No active conversations found.")
        
        sys.exit(0)
    
    # Determine compact mode setting
    compact_mode = args.compact_mode and not args.verbose_mode
    
    # Determine hybrid mode setting
    enable_hybrid_mode = args.enable_hybrid_mode and not args.no_hybrid_mode
    
    # Check if remarkable mode is requested but not available
    if (args.mode in ['remarkable', 'both'] or args.remarkable_document_id) and not REMARKABLE_AVAILABLE:
        print("āŒ Error: reMarkable Cloud integration not available!")
        print("   Install with: pip install -r requirements-remarkable.txt")
        print("   Or use local mode: python main.py --mode local")
        sys.exit(1)
    
    # Check if OneDrive mode is requested but not available  
    if args.mode in ['onedrive', 'both', 'mixed'] and not ONEDRIVE_AVAILABLE:
        print("āŒ Error: OneDrive integration not available!")
        print("   Install with: pip install msal requests")
        print("   Or use local mode: python main.py --mode local")
        sys.exit(1)
    
    # Check if mixed mode is requested but not available
    if args.mode == 'mixed' and not MIXED_AVAILABLE:
        print("āŒ Error: Mixed cloud integration not available!")
        print("   Install dependencies with: pip install -r requirements-mixed.txt")
        print("   Or use setup script: ./setup_mixed_mode.sh")
        print("   Or use local mode: python main.py --mode local")
        sys.exit(1)
    
    # Check if mixed mode is requested but reMarkable not available
    if args.mode == 'mixed' and not REMARKABLE_AVAILABLE:
        print("āŒ Error: Mixed mode requires reMarkable Cloud integration!")
        print("   Install with: pip install -r requirements-remarkable.txt")
        print("   Or use onedrive mode: python main.py --mode onedrive")
        sys.exit(1)
    
    # Setup environment
    setup_environment()
    
    # Validate API key
    api_key = validate_api_key(args.api_key)
    
    # Load reMarkable configuration
    remarkable_config = load_remarkable_config(args.remarkable_config)
    
    # Load OneDrive configuration
    onedrive_config = load_onedrive_config(args.onedrive_config)
    
    # Override config with command line arguments
    if args.mode in ['remarkable', 'both', 'mixed'] or args.remarkable_document_id:
        remarkable_config.update({
            'enabled': True,
            'watch_folder_path': args.remarkable_watch_folder,
            'output_folder_path': args.remarkable_output_folder,
            'poll_interval': args.remarkable_poll_interval,
        })
        
        # For mixed mode, we only watch gpt_out folder in reMarkable, not the regular input folder
        if args.mode == 'mixed':
            remarkable_config['watch_folder_path'] = '/gpt_out'  # Force gpt_out folder for mixed mode
        
        if args.remarkable_one_time_code:
            remarkable_config['one_time_code'] = args.remarkable_one_time_code
    
    # Override OneDrive config with command line arguments
    if args.mode in ['onedrive', 'both', 'mixed']:
        onedrive_config.update({
            'enabled': True,
            'watch_folder_path': args.onedrive_watch_folder,
            'output_folder_path': args.onedrive_output_folder,
            'poll_interval': args.onedrive_poll_interval,
        })
        
        # For mixed mode, also include reMarkable input folder configuration
        if args.mode == 'mixed':
            onedrive_config['remarkable_input_folder'] = args.remarkable_watch_folder
            onedrive_config['remarkable_poll_interval'] = args.remarkable_poll_interval
        
        if args.onedrive_client_id:
            onedrive_config['client_id'] = args.onedrive_client_id
    
    # Print banner
    print("=" * 70)
    print("šŸ–‹ļø  E-INK LLM ASSISTANT")
    print("    AI-Powered Handwriting & Drawing Analysis")
    if args.mode == 'mixed':
        print("    with Mixed Cloud Integration (OneDrive + reMarkable Input/Output)")
    elif remarkable_config.get('enabled'):
        print("    with reMarkable Cloud Integration")
    elif onedrive_config.get('enabled'):
        print("    with OneDrive Integration")
    print("=" * 70)
    
    try:
        if args.file:
            # Single file processing mode
            file_path = Path(args.file)
            if not file_path.exists():
                print(f"āŒ Error: File not found: {file_path}")
                sys.exit(1)
            
            print(f"šŸ“„ Single file mode: {file_path.name}")
            result = await process_single_file(
                str(file_path),
                api_key,
                conversation_id=args.conversation_id,
                compact_mode=compact_mode,
                auto_detect_session=not args.no_auto_detect,
                enable_multi_page=not args.no_multi_page,
                max_pages=args.max_pages,
                enable_editing_workflow=not args.no_editing_workflow,
                enable_hybrid_mode=enable_hybrid_mode
            )
            
            if result:
                print(f"āœ… Processing complete!")
                print(f"šŸ“„ Response saved: {Path(result).name}")
            else:
                print(f"āŒ Processing failed")
                sys.exit(1)
        
        elif args.remarkable_document_id:
            # Single reMarkable document processing mode
            print(f"🌐 Single reMarkable document mode: {args.remarkable_document_id}")
            result = await process_single_remarkable_file(
                args.remarkable_document_id, 
                api_key, 
                remarkable_config
            )
            
            if result:
                print(f"āœ… Processing complete!")
                print(f"šŸ“„ Response saved: {Path(result).name}")
            else:
                print(f"āŒ Processing failed")
                sys.exit(1)
        
        else:
            # File watcher mode (default)
            if args.mode == 'mixed':
                # Mixed mode: OneDrive + reMarkable gpt_out watching
                if not onedrive_config.get('client_id'):
                    print("āŒ Error: OneDrive client_id required for mixed mode")
                    print("   Set via --onedrive-client-id or in config file")
                    sys.exit(1)
                
                # Setup reMarkable session for mixed mode
                from mixed_cloud_processor import create_remarkable_session
                
                print("šŸ” Authenticating with reMarkable Cloud...")
                try:
                    remarkable_session = create_remarkable_session(remarkable_config)
                    print("āœ… reMarkable authentication successful")
                except Exception as e:
                    print(f"āŒ Error: Failed to authenticate with reMarkable Cloud: {e}")
                    sys.exit(1)
                
                # Create and start mixed processor
                mixed_processor = create_mixed_processor(
                    onedrive_config, 
                    remarkable_session, 
                    api_key
                )
                await mixed_processor.start_watching()
                
            elif args.mode == 'onedrive':
                # OneDrive only mode
                if not onedrive_config.get('client_id'):
                    print("āŒ Error: OneDrive client_id required for OneDrive mode")
                    print("   Set via --onedrive-client-id or in config file")
                    sys.exit(1)
                
                processor = OneDriveProcessor(onedrive_config, api_key)
                await processor.start_watching()
                
            elif args.mode == 'both':
                # Both reMarkable and OneDrive (run concurrently)
                print("šŸ”„ Starting both reMarkable and OneDrive watchers...")
                
                tasks = []
                
                # Start reMarkable watcher if configured
                if remarkable_config.get('enabled'):
                    remarkable_processor = RemarkableEInkProcessor(
                        api_key=api_key,
                        watch_folder=args.watch_folder,
                        remarkable_config=remarkable_config
                    )
                    tasks.append(remarkable_processor.start_watching(process_existing=not args.no_existing, mode='remarkable'))
                
                # Start OneDrive watcher if configured
                if onedrive_config.get('enabled'):
                    if not onedrive_config.get('client_id'):
                        print("āŒ Error: OneDrive client_id required for both mode")
                        print("   Set via --onedrive-client-id or in config file")
                        sys.exit(1)
                    
                    onedrive_processor = OneDriveProcessor(onedrive_config, api_key)
                    tasks.append(onedrive_processor.start_watching())
                
                if not tasks:
                    print("āŒ Error: No valid configurations for both mode")
                    sys.exit(1)
                
                # Run both watchers concurrently
                await asyncio.gather(*tasks)
                
            elif remarkable_config.get('enabled'):
                # Use enhanced processor with reMarkable support
                processor = RemarkableEInkProcessor(
                    api_key=api_key, 
                    watch_folder=args.watch_folder,
                    remarkable_config=remarkable_config
                )
            else:
                # Use original processor for local-only mode
                watch_folder = args.watch_folder or "./watch"
                processor = EInkLLMProcessor(
                    api_key=api_key, 
                    watch_folder=watch_folder,
                    conversation_id=args.conversation_id,
                    compact_mode=compact_mode,
                    auto_detect_session=not args.no_auto_detect,
                    enable_multi_page=not args.no_multi_page,
                    max_pages=args.max_pages,
                    enable_editing_workflow=not args.no_editing_workflow,
                    enable_hybrid_mode=enable_hybrid_mode
                )
            
            # For non-mixed/non-onedrive/non-both modes, start the processor
            if args.mode not in ['onedrive', 'both', 'mixed']:
                process_existing = not args.no_existing
                
                if hasattr(processor, 'start_watching') and len(processor.start_watching.__code__.co_varnames) > 2:
                    # Enhanced processor with mode support
                    await processor.start_watching(process_existing=process_existing, mode=args.mode)
                else:
                    # Original processor
                    await processor.start_watching(process_existing=process_existing)
    
    except KeyboardInterrupt:
        print(f"\nšŸ‘‹ Goodbye!")
    except Exception as e:
        print(f"\nāŒ Unexpected error: {e}")
        if args.verbose:
            import traceback
            traceback.print_exc()
        sys.exit(1)

Return Value

This function does not return a value. It runs until interrupted by the user (KeyboardInterrupt) or exits with sys.exit() on errors. Side effects include processing files, generating PDFs, updating databases, and syncing with cloud services.

Dependencies

  • asyncio
  • argparse
  • sys
  • os
  • json
  • pathlib
  • dotenv
  • processor
  • session_manager
  • remarkable_processor
  • onedrive_client
  • mixed_cloud_processor
  • conversation_timeline
  • traceback
  • msal
  • requests

Required Imports

import asyncio
import argparse
import sys
import os
import json
from pathlib import Path
from dotenv import load_dotenv
from processor import EInkLLMProcessor
from processor import process_single_file
from session_manager import SessionManager

Conditional/Optional Imports

These imports are only needed under specific conditions:

from conversation_timeline import ConversationTimelineGenerator

Condition: only when --generate-timeline argument is used

Optional
from remarkable_processor import RemarkableEInkProcessor

Condition: only when using reMarkable Cloud mode (--mode remarkable/both/mixed or --remarkable-document-id)

Optional
from remarkable_processor import process_single_remarkable_file

Condition: only when processing single reMarkable document (--remarkable-document-id)

Optional
from onedrive_client import OneDriveClient

Condition: only when using OneDrive mode (--mode onedrive/both/mixed)

Optional
from onedrive_client import OneDriveProcessor

Condition: only when using OneDrive mode (--mode onedrive/both/mixed)

Optional
from mixed_cloud_processor import MixedCloudProcessor

Condition: only when using mixed cloud mode (--mode mixed)

Optional
from mixed_cloud_processor import create_mixed_processor

Condition: only when using mixed cloud mode (--mode mixed)

Optional
from mixed_cloud_processor import create_remarkable_session

Condition: only when using mixed cloud mode (--mode mixed)

Optional
import traceback

Condition: only when verbose mode is enabled (--verbose)

Optional

Usage Example

import asyncio
import sys

# Example 1: Process a single file
sys.argv = ['main.py', '--file', 'drawing.pdf', '--api-key', 'sk-...']
await main()

# Example 2: Start file watcher in local mode
sys.argv = ['main.py', '--watch-folder', './documents']
await main()

# Example 3: Use reMarkable Cloud mode
sys.argv = ['main.py', '--mode', 'remarkable', '--remarkable-one-time-code', 'abc123']
await main()

# Example 4: Continue existing conversation
sys.argv = ['main.py', '--file', 'question.pdf', '--conversation-id', 'conv_20250731_143022_a8f9c2d1']
await main()

# Example 5: List active conversations
sys.argv = ['main.py', '--list-conversations']
await main()

# Example 6: Generate conversation timeline
sys.argv = ['main.py', '--generate-timeline', 'conv_20250731_143022_a8f9c2d1']
await main()

# Example 7: Mixed cloud mode (OneDrive + reMarkable)
sys.argv = ['main.py', '--mode', 'mixed', '--onedrive-client-id', 'azure-client-id']
await main()

# Run with: python -c "import asyncio; from main import main; asyncio.run(main())"

Best Practices

  • Always set OPENAI_API_KEY environment variable or use --api-key argument before running
  • Use --verbose flag for debugging and detailed error messages
  • For production use, configure cloud services via JSON config files rather than command-line arguments
  • Install appropriate dependencies based on mode: requirements-remarkable.txt for reMarkable, msal/requests for OneDrive
  • Use --no-existing flag when starting watcher to avoid processing old files
  • Enable compact mode (default) for optimal e-ink display rendering
  • Use conversation IDs to maintain context across multiple document exchanges
  • Set reasonable --max-pages limit to avoid processing extremely large PDFs
  • For mixed mode, ensure both OneDrive and reMarkable Cloud are properly authenticated
  • Use --list-conversations to track active sessions before starting new ones
  • Handle KeyboardInterrupt gracefully - the application is designed for long-running operation
  • Check for REMARKABLE_AVAILABLE, ONEDRIVE_AVAILABLE, and MIXED_AVAILABLE flags before using respective modes
  • Use --generate-timeline to create visual summaries of conversation history
  • Enable hybrid mode (default) for rich responses with both text and graphics

Similar Components

AI-powered semantic similarity - components with related functionality:

  • function main_v103 70.5% similar

    Asynchronous main entry point for a test suite that validates Mixed Cloud Processor functionality, including authentication, discovery, and dry-run operations for reMarkable and OneDrive integration.

    From: /tf/active/vicechatdev/e-ink-llm/test_mixed_mode.py
  • function run_demo 70.1% similar

    Orchestrates a complete demonstration of an E-Ink LLM Assistant by creating three sample handwritten content files (question, instruction diagram, math problem) and processing each through an AI pipeline.

    From: /tf/active/vicechatdev/e-ink-llm/demo.py
  • function main_v21 69.7% similar

    Asynchronous main function that runs a reMarkable tablet file watcher as a separate process, monitoring a specified folder for new documents, processing them, and uploading responses back to the reMarkable Cloud.

    From: /tf/active/vicechatdev/e-ink-llm/run_remarkable_bridge.py
  • class RemarkableEInkProcessor 68.2% similar

    Enhanced E-Ink LLM Processor that extends EInkLLMProcessor with reMarkable Cloud integration, enabling file processing from both local directories and reMarkable Cloud storage.

    From: /tf/active/vicechatdev/e-ink-llm/remarkable_processor.py
  • function main_v22 65.9% similar

    Command-line entry point for a reMarkable PDF upload tool that handles argument parsing, folder listing, and PDF document uploads to a reMarkable device.

    From: /tf/active/vicechatdev/e-ink-llm/cloudtest/upload_pdf_new.py
← Back to Browse