genankjs
v1.0.6
Published
generate anki card
Downloads
19
Maintainers
Readme
GenAnki.js
A powerful TypeScript library for generating Anki flashcard packages (.apkg files) programmatically, inspired by the Python genanki library.
✨ Features
- 🃏 Create Anki packages - Generate .apkg files programmatically
- 🎵 Rich media support - Audio files (.mp3, .wav, etc.)
- 🖼️ Image support - Images (.jpg, .png, .gif, etc.)
- 📝 Built-in templates - Basic, Cloze, Image Occlusion, and more
- 🎨 Custom styling - Full CSS customization support
- 📚 Multiple decks - Organize cards into different decks
- 🏷️ Tagging system - Organize and categorize your cards
- 💪 TypeScript first - Full type safety and IntelliSense support
- 🧪 Well tested - Comprehensive test coverage
📦 Installation
npm install genankjs🚀 Quick Start
import { Package, Deck, Note, builtin } from 'genankjs';
// Create a new package
const pkg = new Package();
// Create a deck
const deck = new Deck({
name: 'Spanish Vocabulary',
description: 'Basic Spanish words and phrases',
});
// Create notes with the basic model
const notes = [
new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: ['Hola', 'Hello'],
tags: ['spanish', 'greetings'],
}),
new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: ['Gracias', 'Thank you'],
tags: ['spanish', 'politeness'],
}),
];
// Add notes to deck
deck.addNotes(notes, builtin.BASIC_MODEL);
// Add deck and model to package
pkg.addDeck(deck);
pkg.addModel(builtin.BASIC_MODEL);
// Generate the .apkg file
await pkg.writeToFile('spanish.apkg');
console.log('✅ Package created successfully!');📚 Table of Contents
🏗️ Core Classes
Package
The main container for creating .apkg files.
import { Package } from 'genankjs';
const pkg = new Package({
media: [], // Optional: array of media files
});
// Add decks and models
pkg.addDeck(deck);
pkg.addModel(model);
// Add media files
pkg.addMedia({
name: 'audio.mp3',
data: audioBuffer,
});
// Generate package
await pkg.writeToFile('output.apkg');
// or get as buffer
const buffer = await pkg.writeToBuffer();Deck
Represents a collection of flashcards.
import { Deck } from 'genankjs';
const deck = new Deck({
name: 'My Deck',
description: 'Optional description',
deckId: 123456789, // Optional: custom ID
});
// Add notes
deck.addNote(note, model); // model is optional
deck.addNotes([note1, note2], model);
// Get statistics
console.log(`Notes: ${deck.getNoteCount()}`);
console.log(`Cards: ${deck.getCardCount()}`);Note
Represents a single flashcard note.
import { Note } from 'genankjs';
const note = new Note({
modelId: model.modelId,
fields: ['Front content', 'Back content'],
tags: ['tag1', 'tag2'],
guid: 'optional-custom-guid',
});
// Manipulate fields
note.setField(0, 'New front content');
console.log(note.getField(0));
// Manage tags
note.addTag('new-tag');
note.removeTag('old-tag');Model
Defines the structure and appearance of cards.
import { Model } from 'genankjs';
const model = new Model({
name: 'Custom Model',
fields: [{ name: 'Question' }, { name: 'Answer' }, { name: 'Hint' }],
templates: [
{
name: 'Card 1',
qfmt: '{{Question}}<br><small>{{Hint}}</small>',
afmt: '{{FrontSide}}<hr id="answer">{{Answer}}',
},
],
css: `
.card {
font-family: Arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
}
`,
});🎯 Built-in Models
The library includes several pre-defined models similar to Anki's built-in note types:
| Model | Description | Use Case |
| ------------------------------------ | ------------------------------------- | ----------------------------- |
| BASIC_MODEL | Simple front/back cards | Basic vocabulary, definitions |
| BASIC_AND_REVERSED_CARD_MODEL | Creates both directions automatically | Language learning |
| BASIC_OPTIONAL_REVERSED_CARD_MODEL | Optional reverse cards | Flexible vocabulary |
| BASIC_TYPE_IN_THE_ANSWER_MODEL | Requires typing the answer | Active recall practice |
| CLOZE_MODEL | Fill-in-the-blank cards | Context-based learning |
| IMAGE_OCCLUSION_MODEL | Hide parts of images | Visual learning |
Example Usage
import { builtin } from 'genankjs';
// Basic cards
const basicNote = new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: ['What is the capital of France?', 'Paris'],
});
// Cloze deletion
const clozeNote = new Note({
modelId: builtin.CLOZE_MODEL.modelId,
fields: ['The capital of {{c1::France}} is {{c2::Paris}}.', 'Geography fact'],
});
// Add to deck
deck.addNote(basicNote, builtin.BASIC_MODEL);
deck.addNote(clozeNote, builtin.CLOZE_MODEL);
// Don't forget to add models to package
pkg.addModel(builtin.BASIC_MODEL);
pkg.addModel(builtin.CLOZE_MODEL);🎵 Media Support
Add audio, images, and other media files to your cards:
import { readFileSync } from 'fs';
// Read media files
const audioData = readFileSync('pronunciation.mp3');
const imageData = readFileSync('diagram.jpg');
// Add to package
pkg.addMedia({ name: 'pronunciation.mp3', data: audioData });
pkg.addMedia({ name: 'diagram.jpg', data: imageData });
// Reference in cards (media files are numbered starting from 0)
const note = new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: [
'How do you pronounce "bonjour"? [sound:0]',
'Bonjour <img src="1" style="max-width: 200px;">',
],
});Supported Media Types
- Audio: .mp3, .wav, .ogg, .flac
- Images: .jpg, .png, .gif, .webp, .svg
- Video: .mp4, .webm (limited support)
🎨 Advanced Examples
Custom Model with Multiple Templates
const languageModel = new Model({
name: 'Language Learning',
fields: [
{ name: 'Word' },
{ name: 'Translation' },
{ name: 'Example' },
{ name: 'Audio' },
],
templates: [
{
name: 'Recognition',
qfmt: '{{Word}}<br>{{Audio}}',
afmt: '{{FrontSide}}<hr id="answer">{{Translation}}<br><em>{{Example}}</em>',
},
{
name: 'Production',
qfmt: '{{Translation}}',
afmt: '{{FrontSide}}<hr id="answer">{{Word}}<br>{{Audio}}<br><em>{{Example}}</em>',
},
],
css: `
.card {
font-family: 'Segoe UI', sans-serif;
font-size: 18px;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
}
.example { font-style: italic; color: #ccc; }
`,
});Conditional Templates
// Card will only be generated if "Add Reverse" field is not empty
const template = {
name: 'Reverse Card',
qfmt: '{{#Add Reverse}}{{Back}}{{/Add Reverse}}',
afmt: '{{FrontSide}}<hr id="answer">{{Front}}',
};Bulk Import from CSV
import { readFileSync } from 'fs';
import { parse } from 'csv-parse/sync';
// Read CSV file
const csvData = readFileSync('vocabulary.csv', 'utf8');
const records = parse(csvData, { columns: true });
// Create notes from CSV
const notes = records.map(
(record) =>
new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: [record.word, record.translation],
tags: record.tags?.split(',') || [],
}),
);
// Add all notes to deck
deck.addNotes(notes, builtin.BASIC_MODEL);📋 API Reference
Core Types
interface Field {
name: string;
font?: string;
size?: number;
sticky?: boolean;
rtl?: boolean;
ord?: number;
}
interface Template {
name: string;
qfmt: string; // Question format
afmt: string; // Answer format
bqfmt?: string; // Browser question format
bafmt?: string; // Browser answer format
}
interface MediaFile {
name: string;
data: Buffer | Uint8Array;
}
interface NoteConfig {
modelId?: number;
fields: string[];
tags?: string[];
guid?: string;
}
interface DeckConfig {
deckId?: number;
name: string;
description?: string;
}Utility Functions
// Generate unique identifiers
import { generateGuid, generateUniqueGuid } from 'genankjs';
const guid = generateGuid();
const uniqueGuid = generateUniqueGuid();🛠️ Development
Building
npm run buildTesting
npm test # Run tests once
npm run dev # Run tests in watch modeFormatting
npm run format # Format code
npm run check-format # Check formatting🐛 Troubleshooting
Duplicate Detection in Anki
Problem: Anki shows "notes already present in your collection" and skips importing cards.
Why this happens:
- Anki detects duplicates based on the first field content
- You might already have cards with the same content
- The GUID or checksum matches existing notes
Solutions:
- Use unique content (recommended):
const note = new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: [`Hello - ${Date.now()}`, 'Hello'],
tags: ['unique'],
});- Force unique GUIDs:
import { generateUniqueGuid } from 'genankjs';
const note = new Note({
modelId: builtin.BASIC_MODEL.modelId,
fields: ['Hello', 'Hello'],
guid: generateUniqueGuid().toString(),
});- Use custom model IDs:
const customModel = new Model({
modelId: Date.now(), // Unique timestamp-based ID
name: 'My Custom Model',
// ... rest of config
});Common Issues
| Issue | Solution | | -------------------- | -------------------------------------------------------------- | | "Invalid .apkg file" | Ensure all media files are included and field references match | | Large file sizes | Compress media files before adding | | Import errors | Use Anki 2.1+ and check error logs | | Missing cards | Verify model templates and field names |
Getting Help
- 📖 Check the examples directory
- 🧪 Review test files for usage patterns
- 🐛 Open an issue on GitHub for bugs
- 💡 Request features via GitHub discussions
📄 License
MIT License - see LICENSE file for details.
🙏 Acknowledgments
- Inspired by the Python genanki library by Kerrick Staley
- Thanks to the Anki project for comprehensive file format documentation
- Built with TypeScript and modern Node.js practices
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
