# Project export: Earlybird

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Add your class schedule to Google Calendar only using your syllabus, never miss a date again!
- Devpost: https://devpost.com/software/earlybird-z8eva5
- GitHub: https://github.com/hamza-els/CalHacks.git
- Video: https://www.youtube.com/embed/ZbLq49oKxXs?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Amr Ibrahim (28 commits), Hamza (18 commits), Ahmad Reed (11 commits)

## Devpost submission (written by the team)

### Inspiration

Many professors often use different platforms to host their course which makes organizing your schedule a painstaking process. At the same time, manually inputting your schedules into Google Calendar is an almost unreasonable process. So we need a system that allows us to keep track of our schedules without requiring a massive amount of effort.

### What it does

The tool works by pushing information gathered from syllabi into organized Google calendars.

### How we built it

Backend: Python and Flask the basic framework, we also used Google Calendar API to add events to the calendar, as well as Gemini API for NLP and event parsing, and ICS export (icalendar used to send ics to Google Calendar API) Frontend: The main user interface is currently an HTML page, which uses: JavaScript , Google Identity Services for OAuth (client-side sign-in and API calls) Modern HTML/CSS for UI (no React or Tailwind in the current codebase)

### Challenges we ran into

Calendar Titling. Internet latency, which in turn delayed api calls, making the whole testing process very slow. Long wait times for the parser. The events/tasks on the Google Calendar would often have issues with the dates.

### Accomplishments we're proud of

Since the wait time was kind of annoying, we developed a built-in flappy bird game that allows you to play as the parser processes the syllabi information. Additionally, we added a Dark Mode feature since the white-and-blue theme was a bit hard to look at throughout the night.

### What we learned

How to set up APIs, how to effectively use git.

### What's next

We've noticed whenever an AI agent is embedded in a program (specifically for the projects we've seen here), the wait times are usually a bit longer than expected. We think that Earlybird can expand to an extension that allows one to play a short game while any program loads. On the actual product side, we also want to create an extension that automatically detects if there is a syllabus/work schedule on your webpage. You would then simply click on the extension to automatically update your Google Calendar.

## README (from the GitHub repository)

# 📅 Syllabus to Calendar Converter

A web application that extracts events and tasks from academic syllabi and automatically creates them in Google Calendar using AI-powered parsing.

## Features

- 🤖 **AI-Powered Parsing**: Uses Google Gemini AI to intelligently extract events from syllabi
- 📄 **PDF Support**: Upload syllabi as .txt or .pdf files
- 📝 **Smart Categorization**: Distinguishes between events (lectures, labs, exams) and tasks (assignments, projects)
- 🌍 **Timezone Support**: Automatically detects and uses your local timezone
- 📅 **Google Calendar Integration**: Direct integration with Google Calendar
- 🎨 **Modern UI**: Clean, responsive web interface

## Quick Start

### Prerequisites

- Python 3.8+
- Google account
- Gemini API key (free tier available)

### 1. Install Dependencies

```bash
pip install -r requirements.txt
```

### 2. Setup Google Cloud Project

#### Step 1: Create Google Cloud Project

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Click "Select a project" → "New Project"
3. Enter project name (e.g., "Syllabus Calendar") and click "Create"

#### Step 2: Enable APIs

1. In your project, go to "APIs & Services" → "Library"
2. Search for and enable:
   - **Google Calendar API**
   - **Google OAuth2 API**

#### Step 3: Create OAuth Credentials

1. Go to "APIs & Services" → "Credentials"
2. Click "Create Credentials" → "OAuth client ID"
3. Select "Web application"
4. **Add Authorized redirect URI**: `http://localhost:5000/oauth/callback`
5. Click "Create"
6. Download credentials as `credentials.json`
7. Place `credentials.json` in the project root

#### Step 4: Configure OAuth Consent Screen

1. Go to "APIs & Services" → "OAuth consent screen"
2. Select "External" → "Create"
3. Fill in required fields:
   - App name: Syllabus Calendar
   - User support email: your email
   - Developer contact: your email
4. Click "Save and Continue"
5. In "Scopes", click "Add or Remove Scopes"
6. Add scopes:
   - `.../auth/calendar.events`
   - `.../auth/userinfo.email`
   - `.../auth/userinfo.profile`
7. Save and continue
8. Add test users (your email) if needed
9. Back to "Credentials", click "Edit" on your OAuth client
10. Under "Authorized redirect URIs", add: `http://localhost:5000/oauth/callback`
11. Save

### 3. Setup Gemini API (Optional)

1. Go to [Google AI Studio](https://aistudio.google.com/apikey)
2. Click "Create API Key"
3. Copy the API key

### 4. Create .env File

Create a `.env` file in the project root:

```env
GEMINI_API_KEY=your-gemini-api-key-here
SECRET_KEY=dev-secret-key-change-in-production
```

> **Note**: Gemini API is optional - the app falls back to basic date parsing if not provided.

### 5. Run the Application

```bash
python app.py
```

Open your browser and navigate to: **http://localhost:5000**

## Usage

1. **Sign in with Google**: Click "Sign in with Google" in the header
2. **Upload Syllabus**: Click "Choose File" and select your syllabus (.txt or .pdf format)
3. **Parse Events**: Click "Parse Events" to extract events and tasks
4. **Review**: Check the extracted events (indicated as 📅 Event or 📝 Task)
5. **Create Calendar**: Click "Add to Google Calendar" to create events

## Project Structure

```
CalHacks/
├── app.py                  # Flask web server
├── parsers.py              # Event extraction logic (Gemini AI + dateparser)
├── calendar_utils.py       # Google Calendar integration
├── credentials.json        # Google OAuth credentials (add this)
├── token.json             # User authentication token (auto-generated)
├── .env                   # Environment variables (create this)
├── requirements.txt       # Python dependencies
├── templates/
│   └── index.html         # Web UI
└── examples/
    └── sample_syllabus.txt # Sample test file
```

## Features Explained

### Event vs Task

- **Events**: Lectures, labs, discussions, exams, meetings
  - Have specific start and end times
  - Show location if mentioned
  
- **Tasks**: Assignments, projects, homework
  - Only have due dates (all-day events)
  - No time component

### Timezone Handling

The app automatically detects your browser's timezone and uses it for all calendar events. No manual configuration needed!

## Troubleshooting

### "redirect_uri_mismatch" Error

- Make sure `http://localhost:5000/oauth/callback` is added to Authorized redirect URIs in Google Cloud Console

### "access_denied" Error

- Add your email as a test user in OAuth consent screen
- Make sure OAuth consent screen is published or in testing mode

### Events Times Are Wrong

- The app now uses your local timezone automatically
- If issues persist, check your browser's timezone settings

### Gemini API Not Working

- The app automatically falls back to dateparser if Gemini API key is not set
- Gemini API is completely optional for basic functionality

## Requirements

- Python 3.8+
- Google Cloud account
- Google account for calendar access
- Gemini API key (optional, free tier available)

## Technologies Used

- **Backend**: Flask (Python)
- **AI Parsing**: Google Gemini AI
- **Calendar**: Google Calendar API
- **Date Parsing**: dateparser
- **Frontend**: HTML/CSS/JavaScript

## License

See LICENSE file for details.


## Detected evidence (automated analysis)

Indexed codebase: 7 recognized source files, 133 KB.
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- JavaScript (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (18 of 18)

```
.gitignore
app.py
calendar_utils.py
examples/61A Syllabus.txt
examples/ENG125 Syllabus.txt
examples/FMS_300___syllabus___spring_25a-1.txt
examples/general_simple.txt
examples/math55_syllabus.txt
examples/sample_syllabus.txt
examples/sample2.txt
examples/sample3.txt
image_processor.py
LICENSE
main.py
parsers.py
README.md
requirements.txt
templates/index.html
```

### Dependencies

- requirements.txt: dateparser@>=1.1.3, flask@>=2.3.0, google-api-python-client@>=2.70.0, google-auth-httplib2@>=0.1.0, google-auth-oauthlib@>=1.0.0, google-generativeai@>=0.8.0, icalendar@>=4.1, pdfplumber@>=0.10.0, pillow@>=10.0.0, python-dateutil@>=2.8.2, python-dotenv@>=1.0.0, werkzeug@>=2.3.0

### Recent commits (newest first)

- I dont even know bruh Merge branch 'main' of https://github.com/hamza-els/CalHacks
- added gemini 3 model
- Imrpoved token storage!
- Altered prompt
- Removed Unecessary files
- Inshallah I actually fixed the calendar naming
- Fix on calendar naming issue
- some better titling and QOL changes
- Merge branch 'main' of https://github.com/hamza-els/CalHacks
- Dark mode and night theme
- some better titling, trying to fix some bugs
- Merge branch 'main' of https://github.com/hamza-els/CalHacks
- Better logo + image parsing works
- title is finallyw working
- Sorta fixed calendar naming issue
- Merge branch 'main' of https://github.com/hamza-els/CalHacks
- Fixed Most Issues After Merging
- Merge branch 'main' of https://github.com/hamza-els/CalHacks
- Merge remote-tracking branch 'refs/remotes/origin/main'
- added favicon logo to tab

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
google-api-python-client>=2.70.0
google-auth-httplib2>=0.1.0
google-auth-oauthlib>=1.0.0
icalendar>=4.1
dateparser>=1.1.3
python-dateutil>=2.8.2
flask>=2.3.0
werkzeug>=2.3.0
google-generativeai>=0.8.0
python-dotenv>=1.0.0
pdfplumber>=0.10.0
pillow>=10.0.0
```

### main.py

```python
"""CLI entrypoint for the Event extractor and calendar uploader.

Usage examples:
  python main.py --input examples/sample_input.txt --ics out.ics
  python main.py --input examples/sample_input.txt --google --calendar-id primary

Before using --google, follow README to create `credentials.json` and enable Calendar API.
"""
import argparse
from pprint import pprint
from pathlib import Path
import sys

from parsers import extract_events_from_text

try:
	from calendar_utils import create_google_service, create_google_event, export_events_to_ics
except Exception:
	# We'll only error if the user tries to push or export; keep imports lazy.
	create_google_service = None
	create_google_event = None
	export_events_to_ics = None


def read_input_text(path: str) -> str:
	if path == "-":
		return sys.stdin.read()
	p = Path(path)
	if not p.exists():
		raise FileNotFoundError(f"Input file not found: {path}")
	return p.read_text(encoding="utf-8")


def main():
	parser = argparse.ArgumentParser(description="Extract events from text and create calendar events or export .ics")
	parser.add_argument("--input", "-i", required=True, help="Input text file path or '-' to read stdin")
	parser.add_argument("--google", action="store_true", help="Push extracted events to Google Calendar (requires credentials.json)")
	parser.add_argument("--calendar-id", default="primary", help="Google Calendar ID, default 'primary'")
	parser.add_argument("--ics", help="Write extracted events to an .ics file (Apple Calendar import)")
	parser.add_argument("--dry-run", action="store_true", help="Don't create events; just show parsed results")
	args = parser.parse_args()

	text = read_input_text(args.input)
	events = extract_events_from_text(text)

	if not events:
		print("No event-like dates found in input.")
		return

	print(f"Found {len(events)} events:")
	for i, e in enumerate(events, 1):
		print(f"[{i}] {e['title']}")
		print(f"    start: {e['start']}")
		print(f"    end:   {e['end']}")

	if args.ics:
		if export_events_to_ics is None:
			print("ICS export not available: missing dependencies. Install requirements.txt")
		else:
			out = export_events_to_ics(events, args.ics)
			print(f"Wrote ICS to: {out}")

	if args.google:
		if create_google_service is None:
			print("Google Calendar push not available: missing google client libraries. Install requirements.txt")
			return
		if args.dry_run:
			print("Dry run: skipping Google Calendar push")
			return
		# Create service and push events
		service = create_google_service()
		created = []
		for e in events:
			res = create_google_event(service, e, calendar_id=args.calendar_id)
			created.append(res.get("htmlLink"))
			print("Created:", res.get("htmlLink"))


if __name__ == "__main__":
	main()


```

### app.py

```python
"""Flask web application for extracting events from text and adding to Google Calendar."""
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
from werkzeug.utils import secure_filename
import os
from pathlib import Path
import json
from datetime import datetime
from dotenv import load_dotenv

from parsers import extract_events_from_text, extract_events_with_gemini
from calendar_utils import create_google_service_from_credentials, create_google_event, create_calendar
from image_processor import extract_events_from_image, get_supported_image_formats

# Load environment variables from .env file
load_dotenv()

# Allow HTTP for localhost OAuth (development only!)
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

app = Flask(__name__, static_folder='assets')
app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')

# Configuration
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size

# Create upload folder if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)


def allowed_file(filename):
    """Check if file extension is allowed."""
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS


def get_credentials_from_session():
    """Get credentials from Flask session. Returns Credentials object or None."""
    from google.oauth2.credentials import Credentials
    
    if 'token_data' not in session:
        print("DEBUG: No token_data in session")
        return None
    
    try:
        creds_dict = session['token_data'].copy()  # Make a copy to avoid mutating session
        SCOPES = [
            "openid",
            "https://www.googleapis.com/auth/calendar.events",
            "https://www.googleapis.com/auth/calendar",
            "https://www.googleapis.com/auth/userinfo.email",
            "https://www.googleapis.com/auth/userinfo.profile"
        ]
        
        # Keep expiry as string - Credentials.from_authorized_user_info expects string format
        # If it's somehow a datetime object, convert it back to ISO string
        if 'expiry' in creds_dict:
            if isinstance(creds_dict['expiry'], datetime):
                creds_dict['expiry'] = creds_dict['expiry'].isoformat()
            elif not isinstance(creds_dict['expiry'], str):
                # Remove invalid expiry
                print(f"Warning: Invalid expiry type: {type(creds_dict['expiry'])}, removing")
                creds_dict.pop('expiry', None)
        
        # Ensure all required fields are present
        required_fields = ['token', 'token_uri', 'client_id', 'client_secret']
        for field in required_fields:
            if field not in creds_dict or not creds_dict[field]:
                print(f"Warning: Missing required field in session token_data: {field}")
                return None
        
        print(f"DEBUG: Creating credentials from session data with token: {creds_dict.get('token', '')[:20]}...")
        creds = Credentials.from_authorized_user_info(creds_dict, SCOPES)
        print(f"DEBUG: Credentials created successfully, valid: {creds.valid}, expired: {creds.expired if hasattr(creds, 'expired') else 'N/A'}")
        return creds
    except Exception as e:
        print(f"Error loading credentials from session: {e}")
        import traceback
        traceback.print_exc()
        return None


def save_credentials_to_session(credentials):
    """Save credentials to Flask session."""
    creds_dict = {
        'token': credentials.token,
        'refresh_token': credentials.refresh_token,
        'token_uri': credentials.token_uri,
        'client_id': credentials.client_id,
        'client_secret': credentials.client_secret,
        'scopes': list(credentials.scopes) if credentials.scopes else []
    }
    
    # Add expiry if it exists
    if hasattr(credentials, 'expiry') and credentials.expiry:
        if hasattr(credentials.expiry, 'isoformat'):
            creds_dict['expiry'] = credentials.expiry.isoformat()
        else:
            creds_dict['expiry'] = str(credentials.expiry)
    
    session['token_data'] = creds_dict
    # Mark session as modified to ensure it's saved
    session.modified = True


@app.route('/')
def index():
    """Render the main upload page."""
    return render_template('index.html')


@app.route('/auth-status')
def auth_status():
    """Check authentication status and return user info."""
    has_token = 'token_data' in session
    user_info = session.get('user_info', None)
    is_authenticated = False
    
    print(f"Auth status check: has_token={has_token}, session_auth={session.get('authenticated')}")
    
    if has_token:
        try:
            from googleapiclient.discovery import build
            
            creds = get_credentials_from_session()
            if not creds:
                return jsonify({
                    'authenticated': False,
                    'has_token': False,
                    'user': None
                })
            
            # If credentials are valid (has a token), consider authenticated
            # Check if token exists and hasn't expired
            is_authenticated = creds.valid
            
            if not is_authenticated and creds.token:
                # Token exists, check if expired
                if creds.expired and creds.refresh_token:
                    # Try to refresh
                    try:
                        from google.auth.transport.requests import Request
                        creds.refresh(Request())
                        # Save refreshed credentials back to session
                        save_credentials_to_session(creds)
                        is_authenticated = True
                    except Exception as e:
                        # If refresh fails due to network issues, still consider a
[truncated — 17815 more characters]
```

### parsers.py

```python
"""Simple NLP/parser utilities to extract event-like mentions from free text.

This is intentionally lightweight for a demo: it uses dateparser.search.search_dates
to locate date/time mentions and then builds simple event dicts (title, start, end,
description, location).

Improve by using an NLP model or rule engine for production.
"""
from typing import List, Dict, Optional
import dateparser.search
from dateparser import parse as parse_date
from datetime import timedelta, datetime
import os
import json


def _extract_sentence(text: str, start_idx: int, end_idx: int) -> str:
    # Return the sentence containing the matched span (approximate).
    # Split on line breaks or punctuation to keep it simple.
    left = text.rfind("\n", 0, start_idx)
    if left == -1:
        left = 0
    else:
        left += 1
    right = text.find("\n", end_idx)
    if right == -1:
        right = len(text)
    return text[left:right].strip()


def extract_events_from_text(text: str, base_date: Optional[str] = None) -> List[Dict]:
    """Extract a list of event dicts from free text.

    Returns events with keys: title, start (datetime), end (datetime), description, location

    base_date: optional reference date for relative date parsing (ISO string) — passed to dateparser.
    """
    settings = {"PREFER_DATES_FROM": "future"}
    if base_date:
        settings["RELATIVE_BASE"] = base_date

    # search_dates returns tuples (matched_text, datetime)
    found = dateparser.search.search_dates(text, settings=settings, add_detected_language=False)
    events = []
    if not found:
        return events

    # To avoid duplicating overlapping matches, we'll iterate and create an event per match.
    for match_text, dt in found:
        # find indices to extract context
        idx = text.find(match_text)
        if idx == -1:
            title = match_text
        else:
            title = _extract_sentence(text, idx, idx + len(match_text))

        # Heuristic: if title is long, clip to first clause
        if len(title) > 140:
            title = title.split(".")[0]

        start = dt
        # If no explicit end time, default to 1 hour event
        end = start + timedelta(hours=1)

        event = {
            "title": title or match_text,
            "start": start,
            "end": end,
            "description": title,
            "location": None,
        }
        events.append(event)

    return events


def extract_events_with_gemini(text: str, base_date: Optional[str] = None) -> List[Dict]:
    """Extract events using Google Gemini API for better semantic understanding.
    
    Requires GEMINI_API_KEY environment variable to be set.
    Falls back to dateparser if Gemini API is not available.
    
    Returns events with keys: title, start (datetime), end (datetime), description, location
    """
    api_key = os.environ.get('GEMINI_API_KEY')
    
    if not api_key:
        print("GEMINI_API_KEY not set, falling back to dateparser")
        return extract_events_from_text(text, base_date)
    
    try:
        import google.generativeai as genai
        
        # Configure API - use stable v1 API instead of v1beta
        genai.configure(api_key=api_key)
        
        # List available models to debug
        try:
            available_models = [m.name for m in genai.list_models()]
            print(f"Available models: {available_models}")
        except Exception as e:
            print(f"Could not list models: {e}")
        
        # Build the prompt
        current_date = datetime.now().isoformat() if not base_date else base_date
        prompt = f"""You are an expert at extracting calendar events from academic syllabi and course schedules.

Extract all events, deadlines, exams, lectures, and important dates from the following text. The same event can be referenced in various parts of the text, each with some context. Make sure to group these events if you are sure they are the same

STEPS
- SCAN: Initially go through the text and note important names that refer to events or tasks, make sure to get anything accademically related
- COLLECT: Collect info about events, grouping them by their names (make sure to distinguish seperate midterms)
- COLLECT A SECOND TIME QUICKLY: make sure to not make duplicates
- OUPUT: Output the events with enough info about them to be able to create a simple google calendar event

CLASSIFY each item as either an "event" or "task":
- EVENTS: Have specific start and end times (lectures, labs, discussions, exams, meetings, office hours)
- TASKS: Only have due dates, no specific time needed (assignments, projects, homework, papers)

Return a JSON array. Each item should have:
- type: Either "event" or "task"
- title: A short, descriptive title, do not include the date or time
- start_text: The exact start date/time mentioned (keep original format) OR due date for tasks
- end_text: The exact end date/time mentioned OR EXACTLY 1 HOUR AFTER start_text
- location: Building name, room number, or "Online" if mentioned (usually empty for tasks)
- description: For recurring events/tasks, include days of week (e.g., "Lecture MWF", "Lab TTH", "Assignment Monday"). For non-recurring: Category like "Lecture", "Lab", "Exam", "Discussion", "Assignment", "Project"
- recurring: Boolean indicating if this event/task recurs (e.g., weekly lectures, weekly assignments, recurring meetings). Events/tasks for which there is not specified date but there is a day of the week (or multiple ex: M W (Every Monday and Wednesday)) are likely to be recurring

Important rules:
1. Use the text's actual date formats (don't convert to ISO unless necessary)
2. For events without time, assume reasonable defaults (10am for classes, 3pm for exams)
3. For tasks, use the due date as start_text and set end_text to "0"
4. For recurring events or tasks, include days in description (e.g., "MWF", "TTH", "Monday Wednesday Friday", "Assignment Monday") so the recurrence pattern can be determined
5. Return ON
[truncated — 4618 more characters]
```

### calendar_utils.py

```python
"""Calendar helpers: Google Calendar push and .ics export for Apple Calendar import.

Google Calendar uses OAuth2. Place your OAuth client credentials in `credentials.json`
(downloaded from Google Cloud Console) next to this script. The token is saved to `token.json`.

ICS export uses the `icalendar` package and creates a simple calendar file.
"""
from typing import Dict, List, Optional
import os
from datetime import datetime

try:
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials
    from google_auth_oauthlib.flow import InstalledAppFlow
    from googleapiclient.discovery import build
except Exception:
    # Defer import errors until runtime; requirements not installed during static analysis.
    Credentials = None

from icalendar import Calendar, Event

# If modifying these scopes, delete the file token.json.
SCOPES = [
    "openid",
    "https://www.googleapis.com/auth/calendar.events",
    "https://www.googleapis.com/auth/calendar",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile"
]


def create_google_service_from_credentials(credentials):
    """Create a Google Calendar API service from a credentials object.
    
    Args:
        credentials: google.oauth2.credentials.Credentials object
    
    Returns:
        Google Calendar API service object
    """
    if Credentials is None:
        raise RuntimeError("Google API libraries are not installed. See requirements.txt")
    
    if not credentials:
        raise Exception("Not authenticated. Please sign in with Google first.")
    
    # If credentials are expired, try to refresh
    if credentials.expired and credentials.refresh_token:
        try:
            credentials.refresh(Request())
        except Exception as e:
            print(f"Could not refresh token: {e}")
            raise Exception("Authentication expired. Please sign in again.")
    
    if not credentials.valid:
        raise Exception("Not authenticated. Please sign in with Google first.")
    
    service = build("calendar", "v3", credentials=credentials)
    return service


def create_google_service(credentials_path: str = "credentials.json", token_path: str = "token.json"):
    """Create a Google Calendar API service. Returns the service object.

    Requires `credentials.json` from Google Cloud Console (OAuth Client ID).
    This function will open a browser for the first-time OAuth consent and save `token.json`.
    
    DEPRECATED: Use create_google_service_from_credentials() for session-based auth.
    """
    if Credentials is None:
        raise RuntimeError("Google API libraries are not installed. See requirements.txt")

    creds = None
    if os.path.exists(token_path):
        creds = Credentials.from_authorized_user_file(token_path, SCOPES)
    
    # If there are no (valid) credentials, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            # Refresh the token
            try:
                creds.refresh(Request())
            except Exception as e:
                print(f"Could not refresh token: {e}")
                # If refresh fails, raise an error - user needs to re-authenticate
                raise Exception("Authentication expired. Please sign in again.")
        else:
            # No valid credentials - user needs to authenticate via web OAuth
            raise Exception("Not authenticated. Please sign in with Google first.")

    service = build("calendar", "v3", credentials=creds)
    return service


def create_calendar(service, file_content=None, filename=None) -> tuple:
    """Create a new calendar for syllabus events. ALWAYS creates a new calendar.
    
    service: Google Calendar API service object
    file_content: the actual text content of the uploaded file (optional)
    filename: name of the uploaded file (optional)
    Returns a tuple of (calendar_id, calendar_name).
    """
    calendar_name = "Syllabus Events"
    
    # Log which file we're using
    print(f"DEBUG [CALENDAR UTILS]: Creating calendar with filename: {filename}")
    print(f"DEBUG [CALENDAR UTILS]: file_content length: {len(file_content) if file_content else 0}")
    
    # Generate dynamic calendar name using file content
    if file_content and len(file_content.strip()) > 0:
        try:
            import google.generativeai as genai
            import os
            
            api_key = os.environ.get('GEMINI_API_KEY')
            if api_key:
                genai.configure(api_key=api_key)
                
                # Use first 1300 characters of file content (typically first page)
                content_snippet = file_content[:1300]
                
                print(f"DEBUG [CALENDAR UTILS]: Sending content snippet to Gemini for calendar naming:")
                print(f"DEBUG [CALENDAR UTILS]: Content snippet (first 500 chars): {content_snippet[:500]}")
                print(f"DEBUG [CALENDAR UTILS]: Content snippet length: {len(content_snippet)}")
                
                prompt = f"""Extract the course code and number from this syllabus. This is VERY IMPORTANT.

PRIORITY 1 (HIGHEST PRIORITY): Look for course codes with format [Department][Number]
Examples: "CS 61A", "CS 101", "Math 55", "MATH 1B", "ENG 125", "EECS 16A", "CHEM 1C"
Format is typically: [2-4 letter department code] [number][optional letter]

PRIORITY 2: If no course code is found, extract the course title/topic
Examples: "Discrete Mathematics", "Introduction to Algorithms", "Calculus I"

DO NOT return generic terms like "Math", "Computer Science", "English", "Physics", etc.
DO NOT return the word "syllabus", "course", "class", or "schedule".
DO NOT use cached or remembered information from previous requests.

READ THE SYLLABUS CONTENT BELOW CAREFULLY and extract the specific course information:

Syllabus content:
{content_snippet}

Return ONLY the course code or tit
[truncated — 8188 more characters]
```

### image_processor.py

```python
"""Image processing utilities for extracting events from syllabi images using Gemini Vision.

This module uses Google Gemini's vision capabilities to extract text and events
directly from images, which is more accurate than traditional OCR for complex documents.

Requirements:
    - google-generativeai: Google's Gemini API with vision support
    - pillow: Image processing library
"""

from typing import List, Dict, Optional
from PIL import Image
import io
import os
import threading

from parsers import extract_events_from_text


class TimeoutError(Exception):
    pass


def timeout_handler(func, timeout_seconds=60):
    """Execute a function with a timeout using threading."""
    result = [None]
    exception = [None]
    
    def target():
        try:
            result[0] = func()
        except Exception as e:
            exception[0] = e
    
    thread = threading.Thread(target=target)
    thread.daemon = True
    thread.start()
    
    # Add progress logging
    import time
    check_interval = 30  # Check every 30 seconds
    elapsed = 0
    while thread.is_alive() and elapsed < timeout_seconds:
        thread.join(check_interval)
        elapsed += check_interval
        if thread.is_alive():
            print(f"[Progress] API call still running... ({elapsed}s / {timeout_seconds}s)")
    
    if thread.is_alive():
        raise TimeoutError(f"Function timed out after {timeout_seconds} seconds")
    
    if exception[0]:
        raise exception[0]
    
    return result[0]


def check_gemini_available() -> bool:
    """Check if Gemini API is available.
    
    Returns:
        True if GEMINI_API_KEY is set
    """
    return 'GEMINI_API_KEY' in os.environ


def extract_text_from_image_gemini(image_data: bytes, mime_type: str = 'image/png') -> str:
    """Extract text from an image using Google Gemini Vision API.
    
    Args:
        image_data: The image file as bytes
        mime_type: MIME type of the image (e.g., 'image/png', 'image/jpeg')
        
    Returns:
        Extracted text from the image
        
    Raises:
        RuntimeError: If Gemini API is not available
    """
    if not check_gemini_available():
        raise RuntimeError(
            "GEMINI_API_KEY is not set. "
            "Please set it in your .env file or environment variables."
        )
    
    try:
        import google.generativeai as genai
        
        # Configure API
        api_key = os.environ.get('GEMINI_API_KEY')
        genai.configure(api_key=api_key)
        
        # Try different Gemini models that support vision
        model_names = [
            'models/gemini-1.5-flash',
            'models/gemini-1.5-pro',
            'models/gemini-pro-vision',
            'models/gemini-2.0-flash-exp'
        ]
        
        prompt = """Extract all the text from this image of a syllabus or course schedule. 
Return only the raw text content, preserving the structure and formatting as much as possible.
Include all dates, times, events, assignments, and important information."""
        
        for model_name in model_names:
            try:
                model = genai.GenerativeModel(model_name)
                
                # Configure for text extraction
                generation_config = {
                    "temperature": 0.1,
                    "top_p": 0.8,
                    "top_k": 40,
                    "max_output_tokens": 8192,
                }
                
                # Prepare the image
                image_pil = Image.open(io.BytesIO(image_data))
                
                # Resize image if too large (Gemini has size limits)
                MAX_DIMENSION = 1536  # Gemini's recommended max
                if image_pil.width > MAX_DIMENSION or image_pil.height > MAX_DIMENSION:
                    print(f"Resizing image from {image_pil.size} to fit within {MAX_DIMENSION}x{MAX_DIMENSION}")
                    image_pil.thumbnail((MAX_DIMENSION, MAX_DIMENSION), Image.Resampling.LANCZOS)
                
                # Generate content with image
                response = model.generate_content(
                    [prompt, image_pil],
                    generation_config=generation_config
                )
                
                return response.text
                
            except Exception as e:
                print(f"Model {model_name} failed: {e}")
                continue
        
        raise Exception("All Gemini vision models failed")
        
    except Exception as e:
        raise RuntimeError(f"Failed to extract text from image using Gemini: {str(e)}")


def extract_events_from_image_gemini(image_data: bytes, mime_type: str = 'image/png') -> List[Dict]:
    """Extract events directly from an image using Google Gemini Vision API.
    
    This uses Gemini's vision capabilities to understand the image and extract
    calendar events directly, which is more accurate than OCR + text parsing.
    
    Args:
        image_data: The image file as bytes
        mime_type: MIME type of the image
        
    Returns:
        List of extracted events
    """
    import time
    from datetime import datetime, timedelta
    start_time = time.time()
    print("=" * 60)
    print("Starting image processing with Gemini Vision...")
    print(f"Start time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("=" * 60)
    
    if not check_gemini_available():
        raise RuntimeError(
            "GEMINI_API_KEY is not set. "
            "Please set it in your .env file or environment variables."
        )
    
    try:
        import google.generativeai as genai
        import json
        import dateparser
        
        print("Gemini API key found, configuring API...")
        
        # Configure API
        api_key = os.environ.get('GEMINI_API_KEY')
        genai.configure(api_key=api_key)
        
        print("API configured successfully")
        
        # Build the prompt for event extraction (same as text proces
[truncated — 12726 more characters]
```