https://github.com/nofate/anki-writer
Stream-based anki collection .apkg file generator
https://github.com/nofate/anki-writer
ai-generated anki slop
Last synced: 3 months ago
JSON representation
Stream-based anki collection .apkg file generator
- Host: GitHub
- URL: https://github.com/nofate/anki-writer
- Owner: nofate
- License: mit
- Created: 2025-10-18T20:52:07.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2026-03-29T00:37:34.000Z (4 months ago)
- Last Synced: 2026-03-29T03:14:43.788Z (4 months ago)
- Topics: ai-generated, anki, slop
- Language: TypeScript
- Homepage:
- Size: 142 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# Anki .apkg Writer for TypeScript
[](https://github.com/nofate/anki-writer/actions/workflows/ci.yml)
A modern, streaming TypeScript implementation for generating Anki `.apkg` files without using disk space. Designed for Node.js 22+ with full streaming support for efficient memory usage.
## Features
- **100% Streaming**: No temporary files, everything processed in memory
- **Async Media Handling**: Fetch media from any source (S3, HTTP, filesystem) on-demand
- **Type-Safe**: Full TypeScript support with comprehensive type definitions
- **Zero Dependencies on Python**: Pure TypeScript implementation based on Anki file format
- **Modern Node.js**: Built for Node.js 22+ with native ESM support
- **Memory Efficient**: Streams data directly to output without buffering entire package
- **Flexible Output**: Stream to file, HTTP response, S3, or any WritableStream
## Installation
```bash
npm install anki-apkg-writer
```
## Quick Start
```typescript
import { createAnkiPackage, generateDeckId, generateModelId } from 'anki-apkg-writer';
import { createWriteStream } from 'node:fs';
// Define model (card template)
const model = {
id: generateModelId(),
name: 'Basic',
fields: [
{ name: 'Front', ord: 0 },
{ name: 'Back', ord: 1 }
],
tmpls: [
{
name: 'Card 1',
ord: 0,
qfmt: '{{Front}}',
afmt: '{{FrontSide}}\n\n
\n\n{{Back}}'
}
],
css: '.card { font-family: arial; font-size: 20px; }'
};
// Define deck
const deck = {
id: generateDeckId(),
name: 'My Deck'
};
// Define notes
const notes = [
{ fields: ['Hello', 'Привет'] },
{ fields: ['World', 'Мир'] }
];
// Media resolver (fetch from any source)
const mediaResolver = async (filename) => {
// Return a Readable stream for the media file
// Can be from S3, HTTP, filesystem, etc.
return createReadStream(path.join('media', filename));
};
// Create package
await createAnkiPackage({
model,
deck,
notes,
mediaResolver,
output: createWriteStream('deck.apkg')
});
```
## Advanced Usage
### Stream to HTTP Response (Express)
```typescript
import { createAnkiPackage } from 'anki-apkg-writer';
app.get('/export', async (req, res) => {
res.setHeader('Content-Type', 'application/apkg');
res.setHeader('Content-Disposition', 'attachment; filename="deck.apkg"');
await createAnkiPackage({
model,
deck,
notes,
mediaResolver,
output: res // Stream directly to HTTP response
});
});
```
### Stream to AWS S3
```typescript
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { PassThrough } from 'node:stream';
const s3 = new S3Client({ region: 'us-east-1' });
const passThrough = new PassThrough();
const upload = new Upload({
client: s3,
params: {
Bucket: 'my-bucket',
Key: 'deck.apkg',
Body: passThrough
}
});
await Promise.all([
createAnkiPackage({
model,
deck,
notes,
mediaResolver,
output: passThrough
}),
upload.done()
]);
```
### Fetch Media from S3
```typescript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
const mediaResolver = async (filename) => {
const command = new GetObjectCommand({
Bucket: 'media-bucket',
Key: `audio/${filename}`
});
const response = await s3.send(command);
return response.Body as NodeJS.ReadableStream;
};
```
### Multiple Card Templates
```typescript
const model = {
id: generateModelId(),
name: 'German Word',
fields: [
{ name: 'German', ord: 0 },
{ name: 'English', ord: 1 },
{ name: 'Audio', ord: 2 }
],
tmpls: [
{
name: 'German -> English',
ord: 0,
qfmt: '{{German}} {{Audio}}',
afmt: '{{German}}
{{English}}'
},
{
name: 'English -> German',
ord: 1,
qfmt: '{{English}}',
afmt: '{{English}}
{{German}} {{Audio}}'
}
],
css: '.card { font-family: arial; font-size: 20px; }'
};
```
### Media Files in Notes
```typescript
const notes = [
{
fields: [
'Hund',
'dog',
'[sound:hund.mp3]' // Audio file
]
},
{
fields: [
'Katze',
'cat',
'
' // Image file
]
}
];
```
## API Reference
### `createAnkiPackage(options)`
Creates an Anki package and streams it to the output.
**Options:**
- `model: AnkiModel` - Card model definition
- `deck: AnkiDeck` - Deck configuration
- `notes: AnkiNote[]` - Array of notes to add
- `mediaResolver: MediaResolver` - Function to resolve media files
- `output: NodeJS.WritableStream` - Output stream (file, HTTP response, etc.)
**Returns:** `Promise`
### `generateDeckId()`
Generates a random deck ID (between 2^30 and 2^31).
**Returns:** `number`
### `generateModelId()`
Generates a random model ID.
**Returns:** `number`
### `generateGuid(...fields: any[])`
Generates a GUID for a note based on its fields.
**Returns:** `string`
### `extractMediaFilenames(html: string)`
Extracts media filenames from HTML content.
**Returns:** `string[]`
## Types
### `AnkiModel`
```typescript
interface AnkiModel {
id: number;
name: string;
fields: AnkiField[];
tmpls: AnkiTemplate[];
css: string;
type?: number; // 0 = standard, 1 = cloze
sortf?: number; // Sort field index
}
```
### `AnkiField`
```typescript
interface AnkiField {
name: string;
ord: number;
font?: string;
size?: number;
sticky?: boolean;
rtl?: boolean;
}
```
### `AnkiTemplate`
```typescript
interface AnkiTemplate {
name: string;
ord: number;
qfmt: string; // Question format (HTML)
afmt: string; // Answer format (HTML)
}
```
### `AnkiDeck`
```typescript
interface AnkiDeck {
id: number;
name: string;
desc?: string;
}
```
### `AnkiNote`
```typescript
interface AnkiNote {
fields: string[];
tags?: string[];
guid?: string;
}
```
### `MediaResolver`
```typescript
type MediaResolver = (filename: string) => Promise;
```
## File Format
The `.apkg` file is a ZIP archive containing:
1. **collection.anki2** - SQLite database with:
- `col` table: Collection metadata
- `notes` table: Note data
- `cards` table: Card data
- `revlog` table: Review history
- `graves` table: Deleted items
2. **media** - JSON mapping of media indices to filenames
```json
{
"0": "audio1.mp3",
"1": "image1.jpg"
}
```
3. **0, 1, 2, ...** - Media files with numeric names
## Architecture
- **database.ts**: SQLite database creation using sql.js (in-memory)
- **zip-writer.ts**: Streaming ZIP creation using fflate
- **utils.ts**: ID generation, GUID creation, media extraction
- **types.ts**: TypeScript type definitions
- **index.ts**: Main package writer class
## Performance
- Memory usage scales with concurrent media file processing
- Database built entirely in memory (no disk I/O)
- ZIP streaming prevents buffering entire package
- Suitable for serverless environments (AWS Lambda, etc.)
## Comparison with genanki
| Feature | genanki (Python) | anki-apkg-writer |
|---------|------------------|------------------|
| Language | Python | TypeScript |
| Disk Usage | Temporary files | Zero |
| Streaming | No | Yes |
| Memory | High | Low |
| Dependencies | Many | Few |
| Node.js Native | No | Yes |
## Requirements
- Node.js 22 or higher
- TypeScript 5.6+ (for development)
## License
MIT
## Contributing
Contributions are welcome! Please open an issue or PR.
## Credits
Based on the Anki file format and inspired by the [genanki](https://github.com/kerrickstaley/genanki) Python package.