TLD2 API Reference

Overview

This document provides a complete reference for TLD2's internal APIs, message passing protocol, and developer interfaces. These APIs power communication between the background service worker, content scripts, and sidebar UI.

API Architecture

  • Message Passing: chrome.runtime.sendMessage for async component communication
  • Content Extraction: Readability.js integration via content script injection
  • Summarization: Chrome AI Summarizer API and Gemini API dual-mode
  • TTS: StreamingKokoroJS neural synthesis with Web Audio playback
  • Storage: chrome.storage.local (settings, cache) and chrome.storage.sync (preferences)

Base URL

chrome-extension://[extension-id]/

All internal APIs use chrome.runtime messaging. No external HTTP endpoints.

Message Passing Protocol

TLD2 uses Chrome's message passing API for communication between extension components.

Protocol Structure

interface Message {
  action: string;        // Action identifier (e.g., 'extractContent', 'summarize')
  data?: any;           // Optional payload data
  tabId?: number;       // Optional tab ID for content script targeting
}

interface Response {
  success: boolean;     // Operation success status
  data?: any;          // Response payload
  error?: string;      // Error message if success = false
}

Sending Messages

// From content script or sidebar to background
const response = await chrome.runtime.sendMessage({
  action: 'extractContent',
  data: { url: window.location.href }
});

if (response.success) {
  console.log('Content:', response.data);
} else {
  console.error('Error:', response.error);
}

// From background to specific tab (content script)
const response = await chrome.tabs.sendMessage(tabId, {
  action: 'highlightText',
  data: { selector: '.article-content' }
});

Message Handlers (Background)

// background/service-worker.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  const { action, data } = message;

  // Route to appropriate handler
  switch (action) {
    case 'extractContent':
      handleExtractContent(data, sender.tab.id)
        .then(result => sendResponse({ success: true, data: result }))
        .catch(error => sendResponse({ success: false, error: error.message }));
      return true;  // Keep channel open for async response

    case 'summarize':
      handleSummarize(data)
        .then(result => sendResponse({ success: true, data: result }))
        .catch(error => sendResponse({ success: false, error: error.message }));
      return true;

    case 'generateTTS':
      handleGenerateTTS(data)
        .then(result => sendResponse({ success: true, data: result }))
        .catch(error => sendResponse({ success: false, error: error.message }));
      return true;

    default:
      sendResponse({ success: false, error: 'Unknown action' });
  }
});

Supported Actions

Action Direction Data Response
extractContent Sidebar → Background { tabId: number } { title, content, url }
summarize Sidebar → Background { content, length } { summary: string }
generateTTS Sidebar → Background { text, voice, speed } { audio: Float32Array }
getSettings Any → Background {} { settings: object }
updateSettings Sidebar → Background { settings: object } { success: true }
openSidebar Background → System { tabId: number } { success: true }

Content Extraction API

Extracts clean article content from web pages using Readability.js.

extractContent(tabId)

Extracts main article content from the specified tab.

Parameters

Parameter Type Required Description
tabId number Yes Chrome tab ID to extract content from

Returns

interface ExtractedContent {
  title: string;        // Article title
  content: string;      // Main text content (plain text)
  textContent: string;  // Same as content (legacy compatibility)
  length: number;       // Content length in characters
  excerpt: string;      // Short excerpt/description
  byline?: string;      // Author byline
  url: string;          // Page URL
  siteName?: string;    // Site name
}

Example

async function extractArticle() {
  const [currentTab] = await chrome.tabs.query({
    active: true,
    currentWindow: true
  });

  const response = await chrome.runtime.sendMessage({
    action: 'extractContent',
    data: { tabId: currentTab.id }
  });

  if (response.success) {
    const { title, content, url } = response.data;
    console.log(`Extracted "${title}" from ${url}`);
    console.log(`Content length: ${content.length} characters`);
    return response.data;
  } else {
    throw new Error(response.error);
  }
}

Error Codes

Error Description
NO_TAB Invalid or missing tab ID
NO_CONTENT No extractable content found (PDF, image, etc.)
EXTRACTION_FAILED Readability.js parsing failed
PERMISSION_DENIED Cannot access tab (chrome:// or file:// URL)

Implementation Details

// background/service-worker.js
async function handleExtractContent(data, tabId) {
  // Inject Readability.js into tab
  await chrome.scripting.executeScript({
    target: { tabId: tabId },
    files: ['lib/readability/Readability.js']
  });

  // Execute extraction
  const results = await chrome.scripting.executeScript({
    target: { tabId: tabId },
    func: () => {
      // Clone document for Readability
      const documentClone = document.cloneNode(true);
      const reader = new Readability(documentClone);
      const article = reader.parse();

      if (!article) {
        throw new Error('NO_CONTENT');
      }

      return {
        title: article.title || document.title,
        content: article.textContent,
        textContent: article.textContent,
        length: article.length,
        excerpt: article.excerpt,
        byline: article.byline,
        url: window.location.href,
        siteName: article.siteName
      };
    }
  });

  return results[0].result;
}

Summarization API

Generates concise summaries using Chrome AI or Gemini API.

summarize(content, options)

Generates a summary of the provided content.

Parameters

Parameter Type Required Description
content string Yes Article content to summarize
length string No 'short' | 'medium' | 'long' (default: 'medium')
provider string No 'chrome-ai' | 'gemini' (default: 'chrome-ai')
geminiApiKey string No Required if provider = 'gemini'

Returns

interface SummaryResponse {
  summary: string;      // Generated summary text
  provider: string;     // AI provider used ('chrome-ai' or 'gemini')
  tokens?: number;      // Token count (Gemini only)
  model?: string;       // Model name
}

Example (Chrome AI)

async function summarizeArticle(content) {
  const response = await chrome.runtime.sendMessage({
    action: 'summarize',
    data: {
      content: content,
      length: 'medium',
      provider: 'chrome-ai'
    }
  });

  if (response.success) {
    console.log('Summary:', response.data.summary);
    return response.data.summary;
  } else {
    throw new Error(response.error);
  }
}

Example (Gemini API)

async function summarizeWithGemini(content, apiKey) {
  const response = await chrome.runtime.sendMessage({
    action: 'summarize',
    data: {
      content: content,
      length: 'medium',
      provider: 'gemini',
      geminiApiKey: apiKey
    }
  });

  if (response.success) {
    console.log('Summary from Gemini:', response.data.summary);
    console.log('Tokens used:', response.data.tokens);
    return response.data;
  } else {
    throw new Error(response.error);
  }
}

Summary Length Guidelines

Length Target Words Best For
short 50-100 words Quick overviews, TL;DR
medium 150-300 words Balanced detail (default)
long 300-500 words Comprehensive summaries

Implementation (Chrome AI)

async function summarizeWithChromeAI(content, length) {
  // Check Chrome AI availability
  const availability = await ai.summarizer.availability();

  if (availability === 'no') {
    throw new Error('CHROME_AI_UNAVAILABLE');
  }

  // Download model if needed
  if (availability === 'after-download') {
    console.log('Downloading Chrome AI model...');
    // Model downloads automatically on first use
  }

  // Create summarizer
  const summarizer = await ai.summarizer.create({
    length: length || 'medium',
    format: 'plain-text',
    type: 'tl;dr'
  });

  // Generate summary
  const summary = await summarizer.summarize(content);

  return {
    summary: summary,
    provider: 'chrome-ai',
    model: 'chrome-summarizer-v1'
  };
}

Implementation (Gemini API)

async function summarizeWithGemini(content, length, apiKey) {
  const lengthPrompts = {
    short: 'in 50-100 words',
    medium: 'in 150-300 words',
    long: 'in 300-500 words'
  };

  const prompt = `Summarize the following article ${lengthPrompts[length] || lengthPrompts.medium}. Focus on key points and main ideas:\n\n${content}`;

  const response = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite:generateContent?key=' + apiKey, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      contents: [{
        parts: [{ text: prompt }]
      }]
    })
  });

  const data = await response.json();

  if (data.error) {
    throw new Error(`GEMINI_ERROR: ${data.error.message}`);
  }

  return {
    summary: data.candidates[0].content.parts[0].text,
    provider: 'gemini',
    tokens: data.usageMetadata?.totalTokenCount,
    model: 'gemini-2.0-flash-lite'
  };
}

Text-to-Speech API

Generates neural speech audio using StreamingKokoroJS.

generateTTS(text, options)

Synthesizes speech audio from text input.

Parameters

Parameter Type Required Description
text string Yes Text to synthesize (max 10,000 chars)
voice string No Voice ID (default: 'af_sky')
speed number No Playback speed 0.5-2.0 (default: 1.0)
pitch number No Pitch adjustment 0.5-2.0 (default: 1.0)
streaming boolean No Enable chunk streaming (default: false)

Returns

interface TTSResponse {
  audio: Float32Array;  // Raw audio samples (24kHz, mono)
  duration: number;     // Audio duration in seconds
  voice: string;        // Voice used
  sampleRate: number;   // Sample rate (24000 Hz)
}

Example (Basic)

async function speakText(text) {
  const response = await chrome.runtime.sendMessage({
    action: 'generateTTS',
    data: {
      text: text,
      voice: 'af_sky',
      speed: 1.0
    }
  });

  if (response.success) {
    const { audio, duration } = response.data;
    console.log(`Generated ${duration.toFixed(2)}s of audio`);

    // Play audio
    await playAudioBuffer(audio);
  } else {
    throw new Error(response.error);
  }
}

Example (Streaming)

// For streaming, use port connection instead of sendMessage
const port = chrome.runtime.connect({ name: 'tts-stream' });

port.onMessage.addListener((message) => {
  if (message.type === 'audio-chunk') {
    // Play chunk immediately
    playAudioChunk(message.audio);
  } else if (message.type === 'complete') {
    console.log('TTS streaming complete');
  }
});

port.postMessage({
  action: 'streamTTS',
  data: {
    text: longText,
    voice: 'af_sky',
    speed: 1.2
  }
});

Available Voices

Voice ID Gender Accent Description
af_sky Female American Clear, professional tone (default)
af_nicole Female American Warm, friendly tone
bm_fable Male British Authoritative, narrator style
bm_lewis Male British Conversational, casual

Web Audio Playback Helper

async function playAudioBuffer(audioData) {
  const audioContext = new AudioContext();

  // Create buffer from Float32Array
  const audioBuffer = audioContext.createBuffer(
    1,                    // Mono
    audioData.length,     // Samples
    24000                 // 24kHz sample rate
  );

  // Copy audio data
  audioBuffer.getChannelData(0).set(audioData);

  // Create source and play
  const source = audioContext.createBufferSource();
  source.buffer = audioBuffer;
  source.connect(audioContext.destination);
  source.start();

  // Wait for completion
  return new Promise(resolve => {
    source.onended = resolve;
  });
}

Storage API

TLD2 uses chrome.storage.local for settings and cache, and chrome.storage.sync for cross-device preferences.

Storage Schema

interface StorageSchema {
  // Settings (chrome.storage.sync)
  settings: {
    voice: string;              // Selected voice ID
    speed: number;              // Playback speed (0.5-2.0)
    pitch: number;              // Pitch adjustment (0.5-2.0)
    pitchCorrection: boolean;   // Auto pitch correction
    autoplay: boolean;          // Auto-play summaries
    summaryLength: string;      // 'short' | 'medium' | 'long'
    provider: string;           // 'chrome-ai' | 'gemini'
    geminiApiKey: string;       // Gemini API key
    theme: string;              // 'light' | 'dark' | 'auto'
  };

  // Cache (chrome.storage.local)
  cache: {
    [url: string]: {
      summary: string;
      timestamp: number;
      provider: string;
    }
  };

  // TTS model status (chrome.storage.local)
  ttsModelLoaded: boolean;
}

getSettings()

async function getSettings() {
  const response = await chrome.runtime.sendMessage({
    action: 'getSettings'
  });

  if (response.success) {
    return response.data.settings;
  } else {
    // Return defaults
    return {
      voice: 'af_sky',
      speed: 1.0,
      pitch: 1.0,
      pitchCorrection: true,
      autoplay: true,
      summaryLength: 'medium',
      provider: 'chrome-ai',
      geminiApiKey: '',
      theme: 'dark'
    };
  }
}

updateSettings(settings)

async function updateSettings(newSettings) {
  const response = await chrome.runtime.sendMessage({
    action: 'updateSettings',
    data: { settings: newSettings }
  });

  if (response.success) {
    console.log('Settings saved successfully');
    return true;
  } else {
    throw new Error(response.error);
  }
}

// Usage
await updateSettings({
  voice: 'bm_fable',
  speed: 1.2,
  autoplay: false
});

Direct Storage Access

// Read from storage
const data = await chrome.storage.sync.get('settings');
console.log('Current settings:', data.settings);

// Write to storage
await chrome.storage.sync.set({
  settings: {
    voice: 'af_nicole',
    speed: 1.5
  }
});

// Listen for changes
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync' && changes.settings) {
    console.log('Settings updated:', changes.settings.newValue);
    applyNewSettings(changes.settings.newValue);
  }
});

Cache Management

// Save summary to cache
async function cacheSummary(url, summary, provider) {
  const cache = await chrome.storage.local.get('cache') || {};

  cache[url] = {
    summary: summary,
    timestamp: Date.now(),
    provider: provider
  };

  await chrome.storage.local.set({ cache: cache });
}

// Retrieve cached summary
async function getCachedSummary(url, maxAge = 86400000) {
  const cache = await chrome.storage.local.get('cache') || {};
  const cached = cache[url];

  if (cached && (Date.now() - cached.timestamp) < maxAge) {
    console.log('Using cached summary');
    return cached.summary;
  }

  return null;
}

// Clear old cache entries
async function clearOldCache(maxAge = 604800000) {
  const cache = await chrome.storage.local.get('cache') || {};
  const now = Date.now();

  Object.keys(cache).forEach(url => {
    if (now - cache[url].timestamp > maxAge) {
      delete cache[url];
    }
  });

  await chrome.storage.local.set({ cache: cache });
  console.log('Cache cleared');
}

Settings Management

Default Settings

const DEFAULT_SETTINGS = {
  voice: 'af_sky',
  speed: 1.0,
  pitch: 1.0,
  pitchCorrection: true,
  autoplay: true,
  summaryLength: 'medium',
  provider: 'chrome-ai',
  geminiApiKey: '',
  theme: 'dark'
};

// Initialize settings on first install
chrome.runtime.onInstalled.addListener(async (details) => {
  if (details.reason === 'install') {
    await chrome.storage.sync.set({ settings: DEFAULT_SETTINGS });
    console.log('Default settings initialized');
  }
});

Settings Validation

function validateSettings(settings) {
  const errors = [];

  // Voice validation
  const validVoices = ['af_sky', 'af_nicole', 'bm_fable', 'bm_lewis'];
  if (!validVoices.includes(settings.voice)) {
    errors.push('Invalid voice ID');
  }

  // Speed validation
  if (settings.speed < 0.5 || settings.speed > 2.0) {
    errors.push('Speed must be between 0.5 and 2.0');
  }

  // Pitch validation
  if (settings.pitch < 0.5 || settings.pitch > 2.0) {
    errors.push('Pitch must be between 0.5 and 2.0');
  }

  // Summary length validation
  const validLengths = ['short', 'medium', 'long'];
  if (!validLengths.includes(settings.summaryLength)) {
    errors.push('Invalid summary length');
  }

  // Provider validation
  const validProviders = ['chrome-ai', 'gemini'];
  if (!validProviders.includes(settings.provider)) {
    errors.push('Invalid AI provider');
  }

  // Gemini API key validation
  if (settings.provider === 'gemini' && !settings.geminiApiKey) {
    errors.push('Gemini API key required when using Gemini provider');
  }

  return errors;
}

Error Handling

Error Response Format

interface ErrorResponse {
  success: false;
  error: string;        // Error message
  code?: string;        // Error code
  details?: any;        // Additional error details
}

Standard Error Codes

Code Description Resolution
NO_CONTENT No extractable content found Navigate to article page
CHROME_AI_UNAVAILABLE Chrome AI not available Use Gemini or update Chrome
GEMINI_ERROR Gemini API request failed Check API key and quota
TTS_INIT_FAILED TTS model initialization failed Check GPU support, reload extension
PERMISSION_DENIED Cannot access tab Navigate to accessible page
INVALID_SETTINGS Settings validation failed Check settings values

Error Handling Best Practices

async function handleAction(action, data) {
  try {
    const response = await chrome.runtime.sendMessage({
      action: action,
      data: data
    });

    if (response.success) {
      return response.data;
    } else {
      // Handle specific error codes
      switch (response.code) {
        case 'NO_CONTENT':
          showError('No article found on this page');
          break;

        case 'CHROME_AI_UNAVAILABLE':
          showError('Chrome AI unavailable - switching to Gemini');
          // Fallback to Gemini
          return handleAction(action, { ...data, provider: 'gemini' });

        case 'GEMINI_ERROR':
          showError('Gemini API error - check your API key');
          break;

        default:
          showError(`Error: ${response.error}`);
      }

      throw new Error(response.error);
    }

  } catch (error) {
    console.error('Action failed:', error);
    showError('An unexpected error occurred');
    throw error;
  }
}

function showError(message) {
  const errorDiv = document.getElementById('error-message');
  errorDiv.textContent = message;
  errorDiv.style.display = 'block';

  setTimeout(() => {
    errorDiv.style.display = 'none';
  }, 5000);
}

Next Steps