function main_v68
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.
/tf/active/vicechatdev/e-ink-llm/main.py
146 - 643
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
asyncioargparsesysosjsonpathlibdotenvprocessorsession_managerremarkable_processoronedrive_clientmixed_cloud_processorconversation_timelinetracebackmsalrequests
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
Optionalfrom remarkable_processor import RemarkableEInkProcessor
Condition: only when using reMarkable Cloud mode (--mode remarkable/both/mixed or --remarkable-document-id)
Optionalfrom remarkable_processor import process_single_remarkable_file
Condition: only when processing single reMarkable document (--remarkable-document-id)
Optionalfrom onedrive_client import OneDriveClient
Condition: only when using OneDrive mode (--mode onedrive/both/mixed)
Optionalfrom onedrive_client import OneDriveProcessor
Condition: only when using OneDrive mode (--mode onedrive/both/mixed)
Optionalfrom mixed_cloud_processor import MixedCloudProcessor
Condition: only when using mixed cloud mode (--mode mixed)
Optionalfrom mixed_cloud_processor import create_mixed_processor
Condition: only when using mixed cloud mode (--mode mixed)
Optionalfrom mixed_cloud_processor import create_remarkable_session
Condition: only when using mixed cloud mode (--mode mixed)
Optionalimport traceback
Condition: only when verbose mode is enabled (--verbose)
OptionalUsage 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
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
function main_v103 70.5% similar
-
function run_demo 70.1% similar
-
function main_v21 69.7% similar
-
class RemarkableEInkProcessor 68.2% similar
-
function main_v22 65.9% similar