テクノロジー · 実用ガイド
Ankiのフィルターデッキのテンプレートを学習履歴なしで共有する
既存の検索条件と設定を別のJSONファイルに書き出し、別のAnkiコレクションに再作成します。全文を掲載したローカル実行用ヘルパーが、新しいフィルターデッキを作成する前に一括処理の内容を確認します。

自分が用意した複数のAnkiフィルターデッキをほかの人に再利用してもらいたい場合は、カードとは別にデッキの定義を共有します。以下のヘルパーは、デッキ名、検索条件、上限件数、抽出順序、フィルターデッキの設定をJSONに書き出します。受け取った人はファイルを確認し、自分のコレクション内にある対象カードを使って、まとめて再作成できます。このJSONを通じて、送り手のカードレコードや復習ログが渡ることはありません。
これはAnki Desktop向けの上級者用手順です。使い捨てのコレクションで、実際のAnki 26.09.3のPython/Rustバックエンドを使って検証しています。apply=Trueで設定を取り込むと、新しいフィルターデッキが直ちに構築され、条件に一致するローカルのカードがそこへ移動します。まず事前確認レポートを読み、正しいプロファイルを使用していることを確かめ、変更前に最新のバックアップを用意してください。ヘルパーは以下に全文を掲載しており、Pythonを別途インストールする必要はありません。
1. 必要な移行方法を選ぶ
相手が別のプロファイルで、スケジュール情報を含む自分のコレクション全体を使いたい場合は、既存のコレクションパッケージ形式である.colpkgが適しています。取り込むと、現在のコレクションのカードが置き換わります。フィルターの定義だけを送りたい場合や、蓄積した学習履歴を渡したくない場合には、この方法は適しません。
通常のデッキパッケージである.apkgは、別のコレクションにカードを追加でき、Ankiにはスケジュール情報を除外するオプションがあります。カードを渡すには便利ですが、フィルターデッキの定義を確実に運ぶ形式だと考えないでください。CrowdAnkiもJSONエクスポートからフィルターデッキを明示的に除外しています。タグからフィルターを作るツールや、既存のフィルターを再構築するツールは、この作業の一部には役立ちます。ただし、どちらも選択した複数の既存デッキの完全な定義を移行する操作ではありません。
再利用したいものが設定済みのフィルターデッキなら、掲載のヘルパーを使います。検証した現行版の7つの設定フィールドを保持し、2つの検索条件、明示的なfalseと0の値、現在のプレビュー用待ち時間、古い形式で保存された待ち時間フィールドも含めます。古いフィールドをコピーしても、旧スケジューラーが有効になるわけではありません。通常デッキのプリセット、FSRS設定、アドオン設定、メディア、カードの内容は、このJSON形式の対象外です。
ファイルには、選択したデッキ名と検索条件の文字列そのものが残ります。検索にIDを入れていれば、そのIDも含まれます。共有する前に読み、必要に応じて私的な名前や語句を除いてください。カードや復習履歴がないからといって、これらの文字列が匿名化されるわけではありません。
2. ヘルパーの全文を保存し、Anki Desktopに読み込む
次のexec(...)行全体を、anki_filtered_templates.pyという名前のUTF-8プレーンテキストファイルとして保存してください。プログラム全体がこの1行に入っています。改行と空白をエスケープ表現で保持しているため、このページからコピーしてもPythonのインデントが維持されます。行をそのまま保持し、半角の直線的な引用符を装飾用の引用符に置き換えないでください。元のソースを通常のインデント付き形式で保管しておくこともできます。
exec("\"\"\"Filtered-deck\x20configuration\x20only;\x20tested\x20with\x20Anki\x2026.9.3's\x20real\x20backend.\n\nLoad\x20this\x20file\x20in\x20Anki\x20Desktop's\x20debug\x20console,\x20then\x20use\x20mw.col:\n\x20\x20\x20\x20export_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json',\x20apply=True)\nUse\x20names=['Exact\x20name',\x20...]\x20to\x20export\x20a\x20selection.\x20None\x20exports\x20all\x20filtered\ndecks\x20in\x20name\x20order.\x20prefix\x20changes\x20destination\x20deck\x20names,\x20never\x20searches.\napply=True\x20immediately\x20builds\x20using\x20destination-local\x20cards,\x20in\x20file\x20order.\nEarlier\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.\x20This\x20is\x20not\x20one\x20atomic\nbatch\x20or\x20one\x20promised\x20Undo.\x20Existing\x20filtered\x20decks\x20are\x20never\x20emptied\x20here.\nNo\x20source\x20deck-ID\x20metadata,\x20card/note\x20records\x20or\x20review\x20logs\x20are\x20exported.\nSearch\x20text\x20is\x20preserved\x20and\x20may\x20contain\x20source-specific\x20IDs.\n\"\"\"\nimport\x20json\nimport\x20math\nimport\x20unicodedata\nfrom\x20pathlib\x20import\x20Path\nfrom\x20anki.collection\x20import\x20Collection\nfrom\x20anki.decks\x20import\x20DeckId,\x20FilteredDeckConfig\nfrom\x20google.protobuf.json_format\x20import\x20ParseDict\n\nSCHEMA\x20=\x20'anki-filtered-templates'\nVERSION\x20=\x201\nCONFIG_KEYS\x20=\x20{'reschedule',\x20'search_terms',\x20'delays',\x20'preview_delay',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_again_secs',\x20'preview_hard_secs',\x20'preview_good_secs'}\nDELAY_KEYS\x20=\x20('preview_delay',\x20'preview_again_secs',\x20'preview_hard_secs',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_good_secs')\nORDER\x20=\x20FilteredDeckConfig.SearchTerm.Order\n\n\nclass\x20PartialImportError(RuntimeError):\n\x20\x20\x20\x20\"\"\"Inspect\x20.partial_report;\x20successful\x20earlier\x20creations\x20remain\x20present.\"\"\"\n\x20\x20\x20\x20def\x20__init__(self,\x20message,\x20report):\n\x20\x20\x20\x20\x20\x20\x20\x20self.partial_report\x20=\x20report\n\x20\x20\x20\x20\x20\x20\x20\x20ids\x20=\x20[row['id']\x20for\x20row\x20in\x20report['created']]\n\x20\x20\x20\x20\x20\x20\x20\x20super().__init__(f'{message}.\x20New\x20filtered\x20IDs\x20so\x20far:\x20{ids}.\x20'\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20exception.partial_report;\x20no\x20rollback\x20was\x20attempted.')\n\n\ndef\x20_object(value,\x20allowed,\x20required,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20dict:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20object')\n\x20\x20\x20\x20unknown,\x20missing\x20=\x20set(value)\x20-\x20allowed,\x20required\x20-\x20set(value)\n\x20\x20\x20\x20if\x20unknown\x20or\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20unknown\x20keys\x20{sorted(unknown)},\x20missing\x20keys\x20{sorted(missing)}')\n\n\ndef\x20_name(value):\n\x20\x20\x20\x20if\x20not\x20isinstance(value,\x20str)\x20or\x20not\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Deck\x20name\x20must\x20be\x20a\x20nonempty\x20string')\n\x20\x20\x20\x20if\x20(any(ord(c)\x20\u003c\x2032\x20or\x20ord(c)\x20==\x20127\x20for\x20c\x20in\x20value)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20any(not\x20p\x20or\x20p\x20!=\x20p.strip()\x20for\x20p\x20in\x20value.split('::'))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20unicodedata.normalize('NFC',\x20value)\x20!=\x20value):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20needs\x20nonempty,\x20unpadded,\x20NFC-normalized\x20components:\x20{value!r}')\n\x20\x20\x20\x20return\x20value\n\n\ndef\x20_uint(value,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20int\x20or\x20not\x200\x20\u003c=\x20value\x20\u003c=\x204294967295:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20integer\x20from\x200\x20to\x204294967295')\n\n\ndef\x20_config_json(config):\n\x20\x20\x20\x20#\x20Explicit\x20defaults\x20keep\x20false\x20and\x20zero\x20visible;\x20no\x20IDs\x20or\x20scheduling\x20data.\n\x20\x20\x20\x20result\x20=\x20{'reschedule':\x20config.reschedule,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'search_terms':\x20[{'search':\x20t.search,\x20'limit':\x20t.limit,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'order':\x20ORDER.Name(t.order)}\x20for\x20t\x20in\x20config.search_terms],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'delays':\x20list(config.delays)}\n\x20\x20\x20\x20result.update({key:\x20getattr(config,\x20key)\x20for\x20key\x20in\x20DELAY_KEYS})\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_config(col,\x20value,\x20label):\n\x20\x20\x20\x20_object(value,\x20CONFIG_KEYS,\x20{'search_terms'},\x20label)\n\x20\x20\x20\x20if\x20'reschedule'\x20in\x20value\x20and\x20type(value['reschedule'])\x20is\x20not\x20bool:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.reschedule:\x20expected\x20true\x20or\x20false')\n\x20\x20\x20\x20for\x20key\x20in\x20DELAY_KEYS:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20_uint(value[key],\x20f'{label}.{key}')\n\x20\x20\x20\x20delays\x20=\x20value.get('delays',\x20[])\n\x20\x20\x20\x20if\x20type(delays)\x20is\x20not\x20list\x20or\x20any(type(v)\x20not\x20in\x20(int,\x20float)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20not\x20math.isfinite(v)\x20or\x20v\x20\u003c\x200\x20for\x20v\x20in\x20delays):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.delays:\x20expected\x20finite\x20nonnegative\x20numbers')\n\x20\x20\x20\x20terms\x20=\x20value['search_terms']\n\x20\x20\x20\x20if\x20type(terms)\x20is\x20not\x20list\x20or\x20not\x201\x20\u003c=\x20len(terms)\x20\u003c=\x202:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20requires\x20one\x20or\x20two\x20search\x20terms')\n\x20\x20\x20\x20normalized\x20=\x20[]\n\x20\x20\x20\x20for\x20i,\x20term\x20in\x20enumerate(terms):\n\x20\x20\x20\x20\x20\x20\x20\x20part\x20=\x20f'{label}.search_terms[{i}]'\n\x20\x20\x20\x20\x20\x20\x20\x20_object(term,\x20{'search',\x20'limit',\x20'order'},\x20{'search'},\x20part)\n\x20\x20\x20\x20\x20\x20\x20\x20search\x20=\x20term['search']\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20not\x20isinstance(search,\x20str)\x20or\x20not\x20search.strip():\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20blank\x20search\x20rejected;\x20use\x20explicit\x20deck:*\x20for\x20all\x20decks')\n\x20\x20\x20\x20\x20\x20\x20\x20_uint(term.get('limit',\x200),\x20part\x20+\x20'.limit')\n\x20\x20\x20\x20\x20\x20\x20\x20order\x20=\x20term.get('order',\x200)\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20type(order)\x20is\x20str:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Value(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20elif\x20type(order)\x20is\x20int:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Name(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('not\x20an\x20enum\x20name\x20or\x20integer')\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20(ValueError,\x20KeyError)\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20unknown\x20order\x20{order!r}')\x20from\x20error\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20normalized.append(col.build_search_string(search))\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20invalid\x20search:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20result\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20result.Clear()\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20ParseDict(value,\x20result,\x20ignore_unknown_fields=False)\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20invalid\x20configuration:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20return\x20result,\x20normalized\n\n\ndef\x20_manifest(col,\x20value):\n\x20\x20\x20\x20_object(value,\x20{'schema',\x20'version',\x20'templates'},\x20{'schema',\x20'version',\x20'templates'},\x20'manifest')\n\x20\x20\x20\x20if\x20value['schema']\x20!=\x20SCHEMA\x20or\x20type(value['version'])\x20is\x20not\x20int\x20or\x20value['version']\x20!=\x20VERSION:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Expected\x20schema\x20{SCHEMA!r},\x20version\x20{VERSION}')\n\x20\x20\x20\x20if\x20type(value['templates'])\x20is\x20not\x20list\x20or\x20not\x20value['templates']:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('templates\x20must\x20be\x20a\x20nonempty\x20list')\n\x20\x20\x20\x20seen,\x20result\x20=\x20set(),\x20[]\n\x20\x20\x20\x20for\x20i,\x20item\x20in\x20enumerate(value['templates']):\n\x20\x20\x20\x20\x20\x20\x20\x20_object(item,\x20{'name',\x20'config'},\x20{'name',\x20'config'},\x20f'template\x20{i\x20+\x201}')\n\x20\x20\x20\x20\x20\x20\x20\x20name\x20=\x20_name(item['name'])\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20name.casefold()\x20in\x20seen:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20template\x20name:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20seen.add(name.casefold())\n\x20\x20\x20\x20\x20\x20\x20\x20config,\x20normalized\x20=\x20_config(col,\x20item['config'],\x20name)\n\x20\x20\x20\x20\x20\x20\x20\x20result.append((name,\x20config,\x20normalized))\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_pairs(pairs):\n\x20\x20\x20\x20result\x20=\x20{}\n\x20\x20\x20\x20for\x20key,\x20value\x20in\x20pairs:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20result:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20JSON\x20key:\x20{key}')\n\x20\x20\x20\x20\x20\x20\x20\x20result[key]\x20=\x20value\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_bad_constant(value):\n\x20\x20\x20\x20raise\x20ValueError(f'Nonfinite\x20JSON\x20constant:\x20{value}')\n\n\ndef\x20_counts(col):\n\x20\x20\x20\x20return\x20{'cards':\x20int(col.card_count()),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'revlog':\x20int(col.db.scalar('select\x20count(*)\x20from\x20revlog'))}\n\n\ndef\x20export_filtered_templates(col,\x20path,\x20*,\x20names=None):\n\x20\x20\x20\x20\"\"\"Read\x20existing\x20filtered\x20configs,\x20validate\x20all,\x20then\x20exclusively\x20write\x20JSON.\n\n\x20\x20\x20\x20names=None\x20means\x20all\x20filtered\x20decks;\x20a\x20nonempty\x20list\x20gives\x20exact\x20selection\n\x20\x20\x20\x20and\x20order.\x20The\x20collection\x20is\x20not\x20written.\x20Existing\x20output\x20files\x20are\x20refused.\n\x20\x20\x20\x20Returns\x20the\x20manifest\x20that\x20was\x20written.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20available\x20=\x20{n.name:\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.get(n.id,\x20default=False)['dyn']}\n\x20\x20\x20\x20if\x20names\x20is\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20list(available)\n\x20\x20\x20\x20elif\x20type(names)\x20is\x20list\x20and\x20names\x20and\x20all(isinstance(n,\x20str)\x20for\x20n\x20in\x20names):\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20names\n\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('names\x20must\x20be\x20None\x20or\x20a\x20nonempty\x20list\x20of\x20exact\x20deck\x20names')\n\x20\x20\x20\x20if\x20len(selected)\x20!=\x20len(set(selected)):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Duplicate\x20names\x20in\x20selection')\n\x20\x20\x20\x20missing\x20=\x20[name\x20for\x20name\x20in\x20selected\x20if\x20name\x20not\x20in\x20available]\n\x20\x20\x20\x20if\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Not\x20existing\x20filtered\x20deck\x20names:\x20{missing}')\n\x20\x20\x20\x20manifest\x20=\x20{'schema':\x20SCHEMA,\x20'version':\x20VERSION,\x20'templates':\x20[]}\n\x20\x20\x20\x20for\x20name\x20in\x20selected:\n\x20\x20\x20\x20\x20\x20\x20\x20saved\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(available[name]))\n\x20\x20\x20\x20\x20\x20\x20\x20manifest['templates'].append({'name':\x20saved.name,\x20'config':\x20_config_json(saved.config)})\n\x20\x20\x20\x20_manifest(col,\x20manifest)\x20\x20#\x20No\x20incomplete\x20output\x20when\x20one\x20selection\x20is\x20invalid.\n\x20\x20\x20\x20text\x20=\x20json.dumps(manifest,\x20ensure_ascii=False,\x20allow_nan=False,\x20indent=2)\x20+\x20'\\n'\n\x20\x20\x20\x20with\x20Path(path).open('x',\x20encoding='utf-8')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20stream.write(text)\n\x20\x20\x20\x20return\x20manifest\n\n\ndef\x20import_filtered_templates(col,\x20path,\x20*,\x20prefix='',\x20apply=False):\n\x20\x20\x20\x20\"\"\"Validate\x20the\x20whole\x20file\x20first;\x20apply=False\x20never\x20creates/builds\x20decks.\n\n\x20\x20\x20\x20apply=True\x20creates\x20NEW\x20IDs\x20in\x20manifest\x20order\x20and\x20immediately\x20builds\x20from\n\x20\x20\x20\x20local\x20cards.\x20Prefix\x20alters\x20deck\x20names\x20only.\x20Backend-normalized\x20queries\x20are\n\x20\x20\x20\x20shown\x20in\x20the\x20plan\x20and\x20checked\x20on\x20readback;\x20original\x20query\x20text\x20is\x20submitted.\n\x20\x20\x20\x20On\x20execution\x20failure,\x20PartialImportError.partial_report\x20lists\x20created\x20IDs.\n\x20\x20\x20\x20Earlier\x20successful\x20operations\x20remain;\x20no\x20automatic\x20Empty/delete\x20is\x20done.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20if\x20type(apply)\x20is\x20not\x20bool\x20or\x20not\x20isinstance(prefix,\x20str):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('apply\x20must\x20be\x20a\x20bool\x20and\x20prefix\x20must\x20be\x20a\x20string')\n\x20\x20\x20\x20with\x20Path(path).open(encoding='utf-8-sig')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20data\x20=\x20json.load(stream,\x20object_pairs_hook=_pairs,\x20parse_constant=_bad_constant)\n\x20\x20\x20\x20parsed\x20=\x20_manifest(col,\x20data)\n\x20\x20\x20\x20existing\x20=\x20{n.name.casefold():\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()}\n\x20\x20\x20\x20original_ids\x20=\x20set(existing.values())\n\x20\x20\x20\x20planned\x20=\x20[_name(prefix\x20+\x20name)\x20for\x20name,\x20_,\x20_\x20in\x20parsed]\n\x20\x20\x20\x20folded\x20=\x20{name.casefold()\x20for\x20name\x20in\x20planned}\n\x20\x20\x20\x20if\x20len(folded)\x20!=\x20len(planned):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Destination\x20names\x20collide\x20after\x20adding\x20prefix')\n\x20\x20\x20\x20for\x20name\x20in\x20planned:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(name)\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Destination\x20name\x20already\x20exists:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20components\x20=\x20name.split('::')\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20length\x20in\x20range(1,\x20len(components)):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20parent\x20=\x20'::'.join(components[:length])\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20pid\x20=\x20col.decks.id_for_name(parent)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20parent.casefold()\x20in\x20folded\x20or\x20(pid\x20is\x20not\x20None\x20and\x20col.decks.get(pid)['dyn']):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Filtered\x20decks\x20cannot\x20be\x20parents:\x20{parent}')\n\x20\x20\x20\x20report\x20=\x20{'applied':\x20False,\x20'templates':\x20[],\x20'created':\x20[],\x20'new_parent_decks':\x20[],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'before':\x20_counts(col),\x20'warnings':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Apply\x20immediately\x20builds\x20from\x20destination-local\x20cards\x20in\x20manifest\x20order.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Earlier\x20filtered\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'The\x20batch\x20is\x20not\x20atomic;\x20no\x20automatic\x20rollback\x20or\x20promised\x20single\x20Undo.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Prefix\x20changes\x20filtered\x20names\x20only;\x20search\x20strings\x20keep\x20their\x20original\x20targets.'],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'recovery':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20created\x20IDs\x20and\x20names\x20before\x20any\x20recovery\x20action.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'To\x20return\x20cards,\x20use\x20Empty\x20on\x20ONLY\x20those\x20newly\x20created\x20filtered\x20decks.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Verify\x20each\x20is\x20empty,\x20then\x20delete\x20only\x20those\x20new\x20filtered\x20decks\x20if\x20unwanted.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Do\x20not\x20delete\x20normal\x20parent\x20decks\x20that\x20contain\x20other\x20decks\x20or\x20cards.']}\n\x20\x20\x20\x20for\x20name,\x20(_,\x20config,\x20normalized)\x20in\x20zip(planned,\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20report['templates'].append({'name':\x20name,\x20'config':\x20_config_json(config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'normalized_searches':\x20normalized})\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20normalized\x20!=\x20[term.search\x20for\x20term\x20in\x20config.search_terms]:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['warnings'].append(f'{name}:\x20backend\x20will\x20normalize\x20search\x20spelling;\x20see\x20normalized_searches.')\n\x20\x20\x20\x20if\x20not\x20apply:\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20return\x20report\n\x20\x20\x20\x20report['applied']\x20=\x20True\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20item,\x20(_,\x20config,\x20normalized)\x20in\x20zip(report['templates'],\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(item['name'])\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20appeared\x20after\x20preflight:\x20{item[\"name\"]}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(0))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.name,\x20new.allow_empty\x20=\x20item['name'],\x20True\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.Clear()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20did\x20=\x20int(col.sched.add_or_update_filtered_deck(new).id)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20did\x20in\x20original_ids:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Backend\x20unexpectedly\x20reused\x20existing\x20ID\x20{did}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row\x20=\x20{'id':\x20did,\x20'name':\x20item['name']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['created'].append(row)\x20\x20#\x20Retain\x20the\x20ID\x20even\x20if\x20verification\x20fails.\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20actual\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(did))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row.update(name=actual.name,\x20config=_config_json(actual.config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20card_count=int(col.db.scalar('select\x20count(*)\x20from\x20cards\x20where\x20did=?',\x20did)))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20term,\x20search\x20in\x20zip(expected.search_terms,\x20normalized):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20term.search\x20=\x20search\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20actual.name\x20!=\x20item['name']\x20or\x20actual.config\x20!=\x20expected:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Readback\x20mismatch\x20for\x20new\x20filtered\x20deck\x20{did};\x20inspect\x20partial_report')\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20report['before']\x20!=\x20report['after']:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError('Card/revlog\x20totals\x20changed\x20during\x20import;\x20inspect\x20partial_report')\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20PartialImportError(str(error),\x20report)\x20from\x20error\n\x20\x20\x20\x20finally:\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20created_ids\x20=\x20{row['id']\x20for\x20row\x20in\x20report['created']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['new_parent_decks']\x20=\x20[{'id':\x20n.id,\x20'name':\x20n.name}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20n.id\x20not\x20in\x20original_ids\x20and\x20n.id\x20not\x20in\x20created_ids]\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['inspection_error']\x20=\x20str(error)\n\x20\x20\x20\x20return\x20report\n")
Anki Desktopで移行元のプロファイルを開き、メイン画面に戻ります。Windows/LinuxではCtrl+Shift+;、macOSではCommand+Shift+;でDebug Consoleを開きます。キーボード配列によっては、;の代わりに:または+を使います。上部の入力欄にコマンドを貼り付け、Shiftを押さずにCtrl+ReturnまたはCommand+Returnで実行してください。これは公式のコンソール操作手順です。後述の検証では、GUIのコンソール自体は操作していません。
次のコマンドでファイルを読み込みます。例のパスを、ファイルの絶対パスに置き換えてください。Windowsでも、引用符で囲んだC:/Users/YOU/Downloads/anki_filtered_templates.pyのようなパスではスラッシュを使えます。mw.colは、このAnkiプロファイルで開いているコレクションです。別途インストールしたPythonプロセスから、使用中のコレクションファイルを直接開かないでください。
from pathlib import Path; from aqt import mw; exec(Path("/FULL/PATH/anki_filtered_templates.py").read_text(encoding="utf-8"))
3. カードを準備する前に定義を書き出す
既存のフォルダー内に、新しいJSONファイル名を指定してください。次のコマンドは、現在あるすべてのフィルターデッキを名前順に書き出し、その件数を表示します。エクスポートはコレクションを読み取るだけで、既存の出力ファイルへの上書きは拒否します。自分の環境に合わせてパスを変更してください。
manifest = export_filtered_templates(mw.col, "/FULL/PATH/templates.json"); print(len(manifest["templates"]))
一部だけを選ぶ場合は、構築したい順に正確な名前を指定します。例:manifest = export_filtered_templates(mw.col, "/FULL/PATH/selected-templates.json", names=["Morning", "Exam"])。通常デッキの名前、存在しない名前、重複した名前、空の選択は拒否されます。この例の名前は、実際のフィルターデッキ名に置き換えてください。
JSONをテキストエディターで開いてください。最上位のフィールドはschema、version、templatesで、各テンプレートにはnameとconfigがあります。search_terms内の各検索には、検索文字列を格納するsearchと、limit、orderが入っています。2つ目の検索条件がある場合はそれも確認し、rescheduleとプレビューの値を点検してください。limitが0なら0のまま、reschedule=falseならfalseのまま保持されます。既定値に見えるからといって値を削除せず、生成されたフィールドを残してください。
元のファイルのコピーを保管してください。テンプレートを並べ替えると構築順が変わり、検索条件を変更すると、移行先で集められるカードが変わります。移行元のテンプレートに空の検索条件がある場合、エクスポートは停止し、明示的な検索条件を求めます。deck:*を使うのは、本当にすべてのデッキを対象にしたい場合だけにしてください。エラーを消すためだけに入れてはいけません。
4. 必要なら、スケジュール情報なしのカードパッケージを追加する
受け取る人がすでに適切なカードを持っている場合、この手順は不要です。カードも渡す場合は、JSONを書き出した後、渡したい通常デッキに属するカードを保持している移行元の各フィルターデッキでEmptyを使います。Emptyはカードを削除せず、元のデッキに戻します。これは、移行元コレクションでのカードの現在の配置を意図的に変更する操作です。ヘルパーが代わりに実行することはありません。
元の通常デッキを、対象のサブデッキを含めてAnki Deck Package(.apkg)として書き出します。Include Schedulingをオフにしてください。メディアを含めるかどうかは、カードに必要な内容に応じて選びます。コレクション全体の.colpkgで代用しないでください。Ankiの通常デッキのプリセットを含めるオプションと、JSONで渡すフィルターデッキの定義は別のものです。
この順序には意味があります。実際のバックエンドテストでは、元の通常デッキの階層外にあるフィルターデッキにカードが移っている状態で通常デッキを書き出すと、対象の3枚のうち1枚が欠落しました。また、カードの入った下位のフィルターデッキは、最初のパッケージに通常デッキとして含まれました。移行元のフィルターデッキをEmptyで空にしてから作ったパッケージには、3枚すべてが元の通常デッキに入り、空になったフィルターデッキの定義は含まれませんでした。両パッケージの内部コレクションデータベースを調べたところ、復習ログの行数はどちらも0でした。
受け取る人はJSONより先にAPKGを取り込み、必要なカード、タグ、元の通常デッキの名前を確認します。統合して取り込むからといって、既存ノートが変更されないとは限りません。GUIDが一致するケースのテストでは、移行元のより新しいノート内容に基づいて、移行先のフィールドとタグが取り込み設定どおりに更新されました。一方、移行先の既存カードレコードと復習ログの行は変わりませんでした。受け取る人が関連するカードをすでに持っている場合は、取り込み設定と結果を確認してください。設定専用JSONの影響範囲は、別途行うカードパッケージの取り込みよりも限定されています。
5. 受け取り側のプロファイルでファイル全体を事前確認する
対象の移行先プロファイルに切り替え、コンソールを開き直し、手順2のコマンドでヘルパーを読み込んでください。新しいフィルターデッキを見分けやすくしたい場合は、接頭辞を指定します。Shared::を指定すると、Shared::Morningのような名前になります。親デッキのSharedは通常デッキである必要があり、実際の取り込み時に新しい通常の親デッキが作られることもあります。事前確認自体では何も作成しません。
plan = import_filtered_templates(mw.col, "/FULL/PATH/templates.json", prefix="Shared::"); pp(plan)
予定されているすべてのnameとconfig、normalized_searches、warningsを読んでください。ヘルパーはデッキを1つも作成する前に、全項目を検証します。不正なJSON、重複キー、未知のフィールドやorder値、不正な検索式、空の検索式、無効な名前、移行先での名前の衝突は拒否します。各テンプレートには、空でない検索条件が1つまたは2つ必要です。既存の名前がある場合は、黙って上書きしたり、自動的に+を付けて受け入れたりせず、拒否します。
接頭辞は、deck:Course、tag:chapter1、そのほかの検索条件のどの部分も書き換えません。カードIDを指定するcid:やノートIDを指定するnid:の検索では、移行元のIDがそのまま残ります。そのIDが別のコレクションで意図したレコードを指すとは限りません。検索対象が移行先の意図したレコードを指していることを確認するか、JSONの作業用コピーを意図的に編集して、もう一度事前確認を実行してください。Ankiが有効な検索式の表記を正規化する場合があります。計画にはその結果が表示され、インポーターは作成後、保存された検索式がその結果と一致するか確認します。
事前確認で調べるのは設定と検索構文です。最終的に何枚のカードが集まるかや、スケジューラー固有のすべての構築条件を調べるわけではありません。有効な検索式でも、対象となるカードが1枚も見つからない場合があります。受け取り側と送り手でスケジューラーの設定が異なることもあります。検索順序にretrievabilityを使う場合の検証上の制約は後述します。apply=Trueを使う前に、名前や構文のエラーを解消してください。
6. 一度適用して、作成されたデッキを確認する
レポートに問題がなければ、同じJSONパスと接頭辞を使って次のコマンドを実行してください。このラッパーは、完了結果または途中失敗のレポートを表示し、Ankiの画面更新を要求します。適用は、移行先コレクションを意図的に変更する操作です。誤って移行元プロファイルで実行しないでください。
exec("try:\n\x20\x20\x20\x20result\x20=\x20import_filtered_templates(mw.col,\x20'/FULL/PATH/templates.json',\x20prefix='Shared::',\x20apply=True)\nexcept\x20PartialImportError\x20as\x20error:\n\x20\x20\x20\x20pp(error.partial_report)\n\x20\x20\x20\x20mw.reset()\n\x20\x20\x20\x20raise\nelse:\n\x20\x20\x20\x20pp(result)\n\x20\x20\x20\x20mw.reset()\n")
createdの各項目について、id、name、card_count、configを確認してください。ヘルパーは必ず新しいフィルターデッキIDを要求し、結果を読み返します。保存された名前と設定を確認しますが、計画に示された検索式の正規化は許容します。処理前後でカード数と復習ログ数の合計は一致するはずです。この操作は、移行先にある対象カードを移動させるもので、移行元のカードや復習履歴を取り込むものではありません。
新しいフィルターデッキを開き、検索条件、上限件数、抽出順序、スケジュール変更の設定、プレビュー用待ち時間をJSONと比較してください。条件に一致すると分かっているカードと、そのカードの元のデッキを確認します。全体の件数が同じというだけでは、意図したカードが検索で選ばれた証拠にはなりません。ヘルパーは結果が空でも許可するため、フィルターデッキが空であること自体は異常とは限りません。
JSONと表示された結果を一緒に保管してください。後から別の一式を作りたい場合は、新しい名前か接頭辞を選び、再び事前確認を行います。移行先に同じ名前が残っている状態で同じ取り込みを繰り返すと、拒否されます。このヘルパーは既存デッキをその場で更新するツールではありません。
7. カードの重複、Empty、途中失敗を理解する
複数の検索条件に一致するカードは、先に構築したデッキが優先して取得します。Morningがカード1、2、3を集めた場合、それらがMorningに残っている間、後から構築するExamはカード2と3を同時に集められません。イラストでは、Examにはカード4が入ります。これは説明用の架空のカードラベルであり、実行できる検索式でも、実際のAnkiカードIDでもありません。既存のフィルターデッキが、新しいフィルターにも一致するはずのカードを保持している場合も同様です。
よくある除外対象は、中断中のカード、一時的に非表示にされたカード、すでに別のフィルターデッキに入っているカードです。このほかの対象条件が適用される場合もあります。ヘルパーはカードの中断を解除せず、既存のフィルターを空にすることもありません。結果が少ない、または0枚だからといってデータが欠落したと判断する前に、実際の検索条件、上限件数、対象条件、構築順を確認してください。
reschedule設定は、集めたカードを学習したときの動作を制御します。スケジュール変更が有効な場合、解答によって通常の学習スケジュールが変わることがあります。プレビューモードでは、代わりにプレビュー用の動作が適用されます。テンプレートの移行だけを行うことと、その後でカードを学習することは別です。復習を始める前に、この設定を確認してください。
前のテンプレートが成功した後で適用に失敗すると、すでに作成した新しいデッキは残ります。表示された途中失敗レポートのcreatedとnew_parent_decksを読み、移行先を確認してください。新しいフィルターデッキを使わない場合は、レポートで特定された新規フィルターデッキだけにEmptyを使います。カードが元のデッキに戻り、フィルターデッキが空になったことを確かめてから、不要ならその新規フィルターデッキを削除してください。ほかのカードやデッキを含む通常の親デッキは削除しないでください。
一括処理全体を自動で元に戻す機能はなく、1回のUndoで戻せるという保証もありません。新しいフィルターデッキを空にするとカードは戻りますが、新たに作られたデッキレコードが消えるわけではありません。その後の学習を消去したり、先に行ったAPKGによるノート更新を取り消したり、コレクションのすべての設定を復元したりすることもありません。同じ名前で無条件に再実行せず、原因を解消して、残りの作業を事前確認してください。
8. 検証した範囲と、バージョンに依存する点
通常形式のヘルパーは、一時コレクション内で実際のAnki 26.9.3のPython/Rustバックエンドを使った21件のテストに合格しました。このバックエンドが報告するアプリケーションバージョンは26.09.3です。テストには、20テンプレートのエクスポート、インポート、再エクスポート、2つの検索条件、明示的なfalseと0の値、新規IDの正確な要求、名前と構文のエラー、空でない移行元の履歴、読み取り専用エクスポートと事前確認に対するSQLite全体の論理スナップショット比較が含まれます。バックエンドを通じてカードに解答し、実際の復習ログデータを生成しました。
カードの重複とEmptyのケースでは、実際のバックエンド操作を使いました。途中失敗のテストでは、1件目を実際に作成した後、2件目の作成時に意図的にエラーを挿入しています。確認したのは、レポートと先に成功した処理が残ることです。自然に発生する失敗の頻度を検証したわけではありません。別のレビュー担当者は、さらに4件のバックエンドテストを実行しました。retrievabilityのorder 8による2件目の自然な失敗、後続項目でのフィルターデッキを親にする指定の衝突、後続項目での未知の列挙値、合成の移行元コレクションに存在する実際のカードIDを含む検索式です。自然な失敗では、最初に作成したデッキが残り、正しくレポートされました。2件の事前確認エラーでは、すべてのSQLiteテーブルが変わりませんでした。カードIDのケースでは、cid:の検索文字列がそのまま保持され、空の移行先への取り込みには成功しましたが、集められたカードは0枚でした。これは、構文が有効でも、意図した移行先のカードが集まる保証にはならないことを示しています。別途行った実際のAPKGテストでは、通常のカード共有、GUID一致時の統合、修正したEmpty後に書き出す手順を確認し、そのパッケージを、この掲載ヘルパーそのものと組み合わせました。
最初のテスト環境では、具体的な互換性の制約が見つかりました。FSRSを使っていない新しいコレクションで、移行元のテスト用デッキを構築しようとすると、RETRIEVABILITY_ASCENDINGであるorder 8がAnki内部で失敗しました。これはヘルパーを実行する前の段階です。最終的な20テンプレートの往復テストでは、動作したorder 0から7を使いました。定義されている11種類すべてのorderの名前と数値は、読み取り専用の事前確認に合格しています。ただし、11種類すべてが、あらゆるスケジューラー設定で構築に成功するという意味ではありません。選んだorderで失敗した場合は、エラーとそれ以前に作成されたデッキを確認し、互換性のあるorderを選ぶか、移行先の設定を意図的に合わせてから再試行してください。
プログラムはAnkiのスケジューラーAPIと設定APIを使用しており、これらはバージョンによって変わる可能性があります。検証したバージョンで定義されている7つのフィールドをコピーしますが、将来追加される未知のオプションへの対応は保証しません。AnkiのGUI、実際のユーザープロファイル、モバイルアプリ、複数OSの組み合わせ、大規模な実運用コレクションではテストしていません。ソース、JSON、結果を保管し、Ankiのバージョンを変える際には、必要な確認を再度行ってください。
