{"id":32522878,"url":"https://github.com/nofate/anki-writer","last_synced_at":"2026-05-13T23:31:32.848Z","repository":{"id":319668335,"uuid":"1078984547","full_name":"nofate/anki-writer","owner":"nofate","description":"Stream-based anki collection .apkg file generator","archived":false,"fork":false,"pushed_at":"2026-03-29T00:37:34.000Z","size":145,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-29T03:14:43.788Z","etag":null,"topics":["ai-generated","anki","slop"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nofate.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-18T20:52:07.000Z","updated_at":"2026-03-29T00:37:37.000Z","dependencies_parsed_at":"2025-10-19T22:42:56.488Z","dependency_job_id":null,"html_url":"https://github.com/nofate/anki-writer","commit_stats":null,"previous_names":["nofate/anki-writer"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/nofate/anki-writer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofate%2Fanki-writer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofate%2Fanki-writer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofate%2Fanki-writer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofate%2Fanki-writer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nofate","download_url":"https://codeload.github.com/nofate/anki-writer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nofate%2Fanki-writer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33004122,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-13T13:14:54.681Z","status":"ssl_error","status_checked_at":"2026-05-13T13:14:51.610Z","response_time":115,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai-generated","anki","slop"],"created_at":"2025-10-28T07:21:27.215Z","updated_at":"2026-05-13T23:31:32.843Z","avatar_url":"https://github.com/nofate.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Anki .apkg Writer for TypeScript\n\n[![CI](https://github.com/nofate/anki-writer/actions/workflows/ci.yml/badge.svg)](https://github.com/nofate/anki-writer/actions/workflows/ci.yml)\n\nA 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.\n\n## Features\n\n- **100% Streaming**: No temporary files, everything processed in memory\n- **Async Media Handling**: Fetch media from any source (S3, HTTP, filesystem) on-demand\n- **Type-Safe**: Full TypeScript support with comprehensive type definitions\n- **Zero Dependencies on Python**: Pure TypeScript implementation based on Anki file format\n- **Modern Node.js**: Built for Node.js 22+ with native ESM support\n- **Memory Efficient**: Streams data directly to output without buffering entire package\n- **Flexible Output**: Stream to file, HTTP response, S3, or any WritableStream\n\n## Installation\n\n```bash\nnpm install anki-apkg-writer\n```\n\n## Quick Start\n\n```typescript\nimport { createAnkiPackage, generateDeckId, generateModelId } from 'anki-apkg-writer';\nimport { createWriteStream } from 'node:fs';\n\n// Define model (card template)\nconst model = {\n  id: generateModelId(),\n  name: 'Basic',\n  fields: [\n    { name: 'Front', ord: 0 },\n    { name: 'Back', ord: 1 }\n  ],\n  tmpls: [\n    {\n      name: 'Card 1',\n      ord: 0,\n      qfmt: '{{Front}}',\n      afmt: '{{FrontSide}}\\n\\n\u003chr id=answer\u003e\\n\\n{{Back}}'\n    }\n  ],\n  css: '.card { font-family: arial; font-size: 20px; }'\n};\n\n// Define deck\nconst deck = {\n  id: generateDeckId(),\n  name: 'My Deck'\n};\n\n// Define notes\nconst notes = [\n  { fields: ['Hello', 'Привет'] },\n  { fields: ['World', 'Мир'] }\n];\n\n// Media resolver (fetch from any source)\nconst mediaResolver = async (filename) =\u003e {\n  // Return a Readable stream for the media file\n  // Can be from S3, HTTP, filesystem, etc.\n  return createReadStream(path.join('media', filename));\n};\n\n// Create package\nawait createAnkiPackage({\n  model,\n  deck,\n  notes,\n  mediaResolver,\n  output: createWriteStream('deck.apkg')\n});\n```\n\n## Advanced Usage\n\n### Stream to HTTP Response (Express)\n\n```typescript\nimport { createAnkiPackage } from 'anki-apkg-writer';\n\napp.get('/export', async (req, res) =\u003e {\n  res.setHeader('Content-Type', 'application/apkg');\n  res.setHeader('Content-Disposition', 'attachment; filename=\"deck.apkg\"');\n\n  await createAnkiPackage({\n    model,\n    deck,\n    notes,\n    mediaResolver,\n    output: res  // Stream directly to HTTP response\n  });\n});\n```\n\n### Stream to AWS S3\n\n```typescript\nimport { S3Client } from '@aws-sdk/client-s3';\nimport { Upload } from '@aws-sdk/lib-storage';\nimport { PassThrough } from 'node:stream';\n\nconst s3 = new S3Client({ region: 'us-east-1' });\nconst passThrough = new PassThrough();\n\nconst upload = new Upload({\n  client: s3,\n  params: {\n    Bucket: 'my-bucket',\n    Key: 'deck.apkg',\n    Body: passThrough\n  }\n});\n\nawait Promise.all([\n  createAnkiPackage({\n    model,\n    deck,\n    notes,\n    mediaResolver,\n    output: passThrough\n  }),\n  upload.done()\n]);\n```\n\n### Fetch Media from S3\n\n```typescript\nimport { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';\n\nconst s3 = new S3Client({ region: 'us-east-1' });\n\nconst mediaResolver = async (filename) =\u003e {\n  const command = new GetObjectCommand({\n    Bucket: 'media-bucket',\n    Key: `audio/${filename}`\n  });\n\n  const response = await s3.send(command);\n  return response.Body as NodeJS.ReadableStream;\n};\n```\n\n### Multiple Card Templates\n\n```typescript\nconst model = {\n  id: generateModelId(),\n  name: 'German Word',\n  fields: [\n    { name: 'German', ord: 0 },\n    { name: 'English', ord: 1 },\n    { name: 'Audio', ord: 2 }\n  ],\n  tmpls: [\n    {\n      name: 'German -\u003e English',\n      ord: 0,\n      qfmt: '{{German}} {{Audio}}',\n      afmt: '{{German}}\u003chr id=answer\u003e{{English}}'\n    },\n    {\n      name: 'English -\u003e German',\n      ord: 1,\n      qfmt: '{{English}}',\n      afmt: '{{English}}\u003chr id=answer\u003e{{German}} {{Audio}}'\n    }\n  ],\n  css: '.card { font-family: arial; font-size: 20px; }'\n};\n```\n\n### Media Files in Notes\n\n```typescript\nconst notes = [\n  {\n    fields: [\n      'Hund',\n      'dog',\n      '[sound:hund.mp3]'  // Audio file\n    ]\n  },\n  {\n    fields: [\n      'Katze',\n      'cat',\n      '\u003cimg src=\"katze.jpg\"\u003e'  // Image file\n    ]\n  }\n];\n```\n\n## API Reference\n\n### `createAnkiPackage(options)`\n\nCreates an Anki package and streams it to the output.\n\n**Options:**\n- `model: AnkiModel` - Card model definition\n- `deck: AnkiDeck` - Deck configuration\n- `notes: AnkiNote[]` - Array of notes to add\n- `mediaResolver: MediaResolver` - Function to resolve media files\n- `output: NodeJS.WritableStream` - Output stream (file, HTTP response, etc.)\n\n**Returns:** `Promise\u003cvoid\u003e`\n\n### `generateDeckId()`\n\nGenerates a random deck ID (between 2^30 and 2^31).\n\n**Returns:** `number`\n\n### `generateModelId()`\n\nGenerates a random model ID.\n\n**Returns:** `number`\n\n### `generateGuid(...fields: any[])`\n\nGenerates a GUID for a note based on its fields.\n\n**Returns:** `string`\n\n### `extractMediaFilenames(html: string)`\n\nExtracts media filenames from HTML content.\n\n**Returns:** `string[]`\n\n## Types\n\n### `AnkiModel`\n\n```typescript\ninterface AnkiModel {\n  id: number;\n  name: string;\n  fields: AnkiField[];\n  tmpls: AnkiTemplate[];\n  css: string;\n  type?: number;  // 0 = standard, 1 = cloze\n  sortf?: number;  // Sort field index\n}\n```\n\n### `AnkiField`\n\n```typescript\ninterface AnkiField {\n  name: string;\n  ord: number;\n  font?: string;\n  size?: number;\n  sticky?: boolean;\n  rtl?: boolean;\n}\n```\n\n### `AnkiTemplate`\n\n```typescript\ninterface AnkiTemplate {\n  name: string;\n  ord: number;\n  qfmt: string;  // Question format (HTML)\n  afmt: string;  // Answer format (HTML)\n}\n```\n\n### `AnkiDeck`\n\n```typescript\ninterface AnkiDeck {\n  id: number;\n  name: string;\n  desc?: string;\n}\n```\n\n### `AnkiNote`\n\n```typescript\ninterface AnkiNote {\n  fields: string[];\n  tags?: string[];\n  guid?: string;\n}\n```\n\n### `MediaResolver`\n\n```typescript\ntype MediaResolver = (filename: string) =\u003e Promise\u003cNodeJS.ReadableStream\u003e;\n```\n\n## File Format\n\nThe `.apkg` file is a ZIP archive containing:\n\n1. **collection.anki2** - SQLite database with:\n   - `col` table: Collection metadata\n   - `notes` table: Note data\n   - `cards` table: Card data\n   - `revlog` table: Review history\n   - `graves` table: Deleted items\n\n2. **media** - JSON mapping of media indices to filenames\n   ```json\n   {\n     \"0\": \"audio1.mp3\",\n     \"1\": \"image1.jpg\"\n   }\n   ```\n\n3. **0, 1, 2, ...** - Media files with numeric names\n\n## Architecture\n\n- **database.ts**: SQLite database creation using sql.js (in-memory)\n- **zip-writer.ts**: Streaming ZIP creation using fflate\n- **utils.ts**: ID generation, GUID creation, media extraction\n- **types.ts**: TypeScript type definitions\n- **index.ts**: Main package writer class\n\n## Performance\n\n- Memory usage scales with concurrent media file processing\n- Database built entirely in memory (no disk I/O)\n- ZIP streaming prevents buffering entire package\n- Suitable for serverless environments (AWS Lambda, etc.)\n\n## Comparison with genanki\n\n| Feature | genanki (Python) | anki-apkg-writer |\n|---------|------------------|------------------|\n| Language | Python | TypeScript |\n| Disk Usage | Temporary files | Zero |\n| Streaming | No | Yes |\n| Memory | High | Low |\n| Dependencies | Many | Few |\n| Node.js Native | No | Yes |\n\n## Requirements\n\n- Node.js 22 or higher\n- TypeScript 5.6+ (for development)\n\n## License\n\nMIT\n\n## Contributing\n\nContributions are welcome! Please open an issue or PR.\n\n## Credits\n\nBased on the Anki file format and inspired by the [genanki](https://github.com/kerrickstaley/genanki) Python package.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnofate%2Fanki-writer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnofate%2Fanki-writer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnofate%2Fanki-writer/lists"}