Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions built-in-ai-task-apis-polyfills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ specifically:
- **Rewriter API**
- **Language Detector API**
- **Translator API**
- **Taxonomizer API**
- **Classifier API**

These polyfills are backed by the
[`prompt-api-polyfill`](https://github.com/GoogleChromeLabs/web-ai-demos/tree/main/prompt-api-polyfill),
Expand All @@ -25,7 +25,7 @@ window.Writer;
window.Rewriter;
window.LanguageDetector;
window.Translator;
window.Taxonomizer;
window.Classifier;
```

so you can use these Task APIs even in environments where they are not yet
Expand Down Expand Up @@ -70,8 +70,8 @@ defensive dynamic import strategy:
if (!('Translator' in window)) {
polyfills.push(import('built-in-ai-task-apis-polyfills/translator'));
}
if (!('Taxonomizer' in window)) {
polyfills.push(import('built-in-ai-task-apis-polyfills/taxonomizer'));
if (!('Classifier' in window)) {
polyfills.push(import('built-in-ai-task-apis-polyfills/classifier'));
}
await Promise.all(polyfills);

Expand Down Expand Up @@ -146,16 +146,14 @@ const translator = await Translator.create({
const result = await translator.translate('Hello world');
```

#### Taxonomizer API
#### Classifier API

```js
const taxonomizer = await Taxonomizer.create();
const results = await taxonomizer.categorize('A story about a cat');
const classifier = await Classifier.create();
const results = await classifier.classify('A story about a cat');

for (const { id, confidence } of results) {
console.log(
`${Taxonomizer.getCategoryName(id)} (${(confidence * 100).toFixed(1)}%)`
);
console.log(`${id} (${(confidence * 100).toFixed(1)}%)`);
}
```

Expand Down Expand Up @@ -196,7 +194,7 @@ For complete examples, see:
- [`demo_rewriter.html`](demo_rewriter.html)
- [`demo_language_detector.html`](demo_language_detector.html)
- [`demo_translator.html`](demo_translator.html)
- [`demo_taxonomizer.html`](demo_taxonomizer.html)
- [`demo_classifier.html`](demo_classifier.html)

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
*/

import { BaseTaskModel } from './base-task-model.js';
import { TaxonomizerPromptBuilder } from './taxonomizer-prompt-builder.js';
import { ClassifierPromptBuilder } from './classifier-prompt-builder.js';

/**
* Taxonomizer API Polyfill
* Classifier API Polyfill
* Backed by Prompt API Polyfill (LanguageModel)
*/

export class Taxonomizer extends BaseTaskModel {
export class Classifier extends BaseTaskModel {
#options;

constructor(session, builder, options) {
Expand All @@ -30,7 +30,7 @@ export class Taxonomizer extends BaseTaskModel {
await this.ensureLanguageModel();
this._checkContext();

const builder = new TaxonomizerPromptBuilder();
const builder = new ClassifierPromptBuilder();
const { systemPrompt, initialPrompts } = builder.buildPrompt('');

const sessionOptions = {
Expand All @@ -44,22 +44,22 @@ export class Taxonomizer extends BaseTaskModel {

const win = this.__window || globalThis;
const session = await win.LanguageModel.create(sessionOptions);
const taxonomizer = new this(session, builder, options);
const classifier = new this(session, builder, options);

if (options.signal) {
options.signal.addEventListener(
'abort',
() => {
taxonomizer.destroy(options.signal.reason);
classifier.destroy(options.signal.reason);
},
{ once: true }
);
}

return taxonomizer;
return classifier;
}

async categorize(input, options = {}) {
async classify(input, options = {}) {
this._checkContext();

if (typeof input !== 'string') {
Expand All @@ -81,16 +81,16 @@ export class Taxonomizer extends BaseTaskModel {
} catch (e2) {
const win = this.constructor.__window || globalThis;
const EX = win.DOMException || globalThis.DOMException || Error;
console.error('Failed to parse Taxonomizer results:', resultString);
throw new EX('Failed to parse categorization results.', 'UnknownError');
console.error('Failed to parse Classifier results:', resultString);
throw new EX('Failed to parse classification results.', 'UnknownError');
}
}
}

#parseResults(jsonString) {
let results = JSON.parse(jsonString);
if (!Array.isArray(results)) {
throw new Error('Categorization results must be an array.');
throw new Error('Classification results must be an array.');
}

// 1. Basic cleaning and validation
Expand Down Expand Up @@ -143,16 +143,11 @@ export class Taxonomizer extends BaseTaskModel {

return finalResults;
}

// Static helper as requested by the user
static getCategoryName(id) {
return TaxonomizerPromptBuilder.getCategoryName(id);
}
}

// Global exposure if in browser
BaseTaskModel.exposeAPIGlobally(
'Taxonomizer',
Taxonomizer,
'__FORCE_TAXONOMIZER_POLYFILL__'
'Classifier',
Classifier,
'__FORCE_CLASSIFIER_POLYFILL__'
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

export class TaxonomizerPromptBuilder {
static #systemPromptTemplate = `You are an expert in content categorization using the IAB Content Taxonomy v3.1.
export class ClassifierPromptBuilder {
static #systemPromptTemplate = `You are an expert in content classification using the IAB Content Taxonomy v3.1.
Your task is to analyze the provided text and predict the most relevant categories.
The taxonomy categories are provided as a flat list of Unique IDs and their corresponding Tiered Category paths.

Expand Down Expand Up @@ -749,23 +749,14 @@ JSON Schema for output:
return this.#taxonomyData;
}

static getCategoryName(id) {
// This is part of the requirement: helper to lookup category name based on ID.
const data = this.#taxonomyData;
if (!data || Object.keys(data).length === 0) {
return id;
}
return data[id] || id;
}

buildPrompt(inputText) {
const taxonomyMap = TaxonomizerPromptBuilder.getTaxonomy();
const taxonomyMap = ClassifierPromptBuilder.getTaxonomy();
const taxonomyList = Object.entries(taxonomyMap)
.map(([id, path]) => `${id}: ${path}`)
.join('\n');

return {
systemPrompt: TaxonomizerPromptBuilder.#systemPromptTemplate.replace(
systemPrompt: ClassifierPromptBuilder.#systemPromptTemplate.replace(
'{{taxonomy}}',
taxonomyList
),
Expand All @@ -785,7 +776,7 @@ JSON Schema for output:
]),
},
],
userPrompt: `TEXT TO CATEGORIZE:\n${inputText}`,
userPrompt: `TEXT TO CLASSIFY:\n${inputText}`,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Taxonomizer API Polyfill Demo</title>
<title>Classifier API Polyfill Demo</title>
<style>
:root {
color-scheme: dark light;
Expand Down Expand Up @@ -99,14 +99,14 @@
</head>

<body>
<h1>Taxonomizer API Polyfill Demo</h1>
<h1>Classifier API Polyfill Demo</h1>

<textarea id="input">
The new experimental fusion reactor at the ITER site in France has successfully sustained a stable plasma for 10 minutes, a new world record. This milestone brings us closer to clean, limitless energy. The complex tokamak device uses superconducting magnets to confine the 150 million degree Celsius fuel. Scientists from around the globe are collaborating on this massive physics project, which could revolutionize how we power our world.</textarea
>

<div id="controls">
<button id="categorizeBtn">Categorize Text</button>
<button id="classifyBtn">Classify Text</button>
<progress
id="progress"
value="0"
Expand All @@ -117,9 +117,7 @@ <h1>Taxonomizer API Polyfill Demo</h1>
</div>

<h2>Predicted Categories</h2>
<div id="output" class="output">
Click "Categorize Text" to see results.
</div>
<div id="output" class="output">Click "Classify Text" to see results.</div>

<script type="module">
// Import Firebase config if present (standard pattern for this project)
Expand All @@ -135,31 +133,63 @@ <h2>Predicted Categories</h2>
window.FIREBASE_CONFIG = firebaseConfig;

// Import the polyfill
import { Taxonomizer } from './taxonomizer-api-polyfill.js';
import { Classifier } from './classifier-api-polyfill.js';

const input = document.getElementById('input');
const categorizeBtn = document.getElementById('categorizeBtn');
const classifyBtn = document.getElementById('classifyBtn');
const output = document.getElementById('output');
const progress = document.getElementById('progress');
const status = document.getElementById('status');

let taxonomizer = null;
const TAXONOMY_URL =
'https://raw.githubusercontent.com/InteractiveAdvertisingBureau/Taxonomies/develop/Content%20Taxonomies/Content%20Taxonomy%203.1.tsv';
let taxonomyMap = {};

async function fetchTaxonomy() {
status.textContent = 'Fetching taxonomy...';
const response = await fetch(TAXONOMY_URL);
if (!response.ok) {
throw new Error(`Failed to fetch taxonomy: ${response.statusText}`);
}
const tsv = await response.text();
return parseTSV(tsv);
}

function parseTSV(tsv) {
const lines = tsv.trim().split('\n');
const dataLines = lines.slice(1);
const taxonomy = {};

for (const line of dataLines) {
const [id, parent, name, t1, t2, t3, t4] = line.split('\t');
if (!id) {
continue;
}

const tiers = [t1, t2, t3, t4].filter((t) => t && t.trim() !== '');
taxonomy[id] = tiers.map((t) => t.trim()).join(' > ');
}
return taxonomy;
}

let classifier = null;

async function init() {
try {
status.textContent = 'Checking availability...';
const availability = await Taxonomizer.availability();
status.textContent = 'Initializing...';
taxonomyMap = await fetchTaxonomy();
const availability = await Classifier.availability();

if (availability === 'unavailable') {
status.textContent =
'Error: Built-in AI (LanguageModel) not available in this browser.';
categorizeBtn.disabled = true;
classifyBtn.disabled = true;
return;
}

status.textContent =
'Creating Taxonomizer instance (this may involve downloading a model)...';
taxonomizer = await Taxonomizer.create({
'Creating Classifier instance (this may involve downloading a model)...';
classifier = await Classifier.create({
monitor: (m) => {
progress.style.display = 'block';
m.addEventListener('downloadprogress', (e) => {
Expand All @@ -180,24 +210,24 @@ <h2>Predicted Categories</h2>
}
}

categorizeBtn.onclick = async () => {
if (!taxonomizer) {
status.textContent = 'Taxonomizer not ready.';
classifyBtn.onclick = async () => {
if (!classifier) {
status.textContent = 'Classifier not ready.';
return;
}

const text = input.value.trim();
if (!text) {
alert('Please enter some text to categorize.');
alert('Please enter some text to classify.');
return;
}

output.textContent = 'Analyzing content...';
categorizeBtn.disabled = true;
classifyBtn.disabled = true;

try {
const startTime = performance.now();
const results = await taxonomizer.categorize(text);
const results = await classifier.classify(text);
const endTime = performance.now();
const duration = ((endTime - startTime) / 1000).toFixed(1);

Expand All @@ -206,7 +236,7 @@ <h2>Predicted Categories</h2>
} else {
output.innerHTML = '';
results.forEach((res) => {
const categoryPath = Taxonomizer.getCategoryName(res.id);
const categoryPath = taxonomyMap[res.id] || res.id;
const item = document.createElement('div');
item.className = 'result-item';
item.innerHTML = `
Expand All @@ -219,12 +249,12 @@ <h2>Predicted Categories</h2>
output.appendChild(item);
});
}
status.textContent = `Categorization complete in ${duration}s.`;
status.textContent = `Classification complete in ${duration}s.`;
} catch (err) {
output.textContent = 'Error: ' + err.message;
status.textContent = 'An error occurred during categorization.';
status.textContent = 'An error occurred during classification.';
} finally {
categorizeBtn.disabled = false;
classifyBtn.disabled = false;
}
};

Expand Down
4 changes: 2 additions & 2 deletions built-in-ai-task-apis-polyfills/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import './writer-api-polyfill.js';
import './rewriter-api-polyfill.js';
import './language-detector-api-polyfill.js';
import './translator-api-polyfill.js';
import './taxonomizer-api-polyfill.js';
import './classifier-api-polyfill.js';

export { Summarizer } from './summarizer-api-polyfill.js';
export { Writer } from './writer-api-polyfill.js';
export { Rewriter } from './rewriter-api-polyfill.js';
export { LanguageDetector } from './language-detector-api-polyfill.js';
export { Translator } from './translator-api-polyfill.js';
export { Taxonomizer } from './taxonomizer-api-polyfill.js';
export { Classifier } from './classifier-api-polyfill.js';
4 changes: 2 additions & 2 deletions built-in-ai-task-apis-polyfills/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"./rewriter": "./dist/rewriter.js",
"./language-detector": "./dist/language-detector.js",
"./translator": "./dist/translator.js",
"./taxonomizer": "./dist/taxonomizer.js"
"./classifier": "./dist/classifier.js"
},
"files": [
"dist/",
Expand Down Expand Up @@ -48,7 +48,7 @@
"rewriter",
"translator",
"language-detector",
"taxonomizer",
"classifier",
"web-ai"
],
"homepage": "https://github.com/GoogleChromeLabs/web-ai-demos/tree/main/built-in-ai-task-apis-polyfills#readme",
Expand Down
Loading