
AnkiAI-Cards - мобильное Android приложение с помощью ИИ генерирует карточки по шаблону и отправляет их напрямую в AnkiDroid внутри смартфона. Помогает изучать иностранные слова ассоциируя с контекстом фраз и предложений.
Глава 1 - начало
В потугах сдвинуть свой английский с B1+ дабы обуздать уровень B2 я ринулся искать пути. У меня была привычка каждый день заходить в Anki, повторять слова но карточки были скудными. Было принято решение искать готовые шаблоны либо сделать свой. Опосля, в поисках меня вдруг озарило увидев шаблоны без перевода на родной язык, с конструкцией cloze (ключевое слово скрывается от глаз). О чудо! Мне определено нужны карточки в которых слово вспоминается по контексту и описанию! Для удобства "ключевое слово" в этой статье назову - "угадайка"
Шаблон для карточек
Позаимствовав все готовые примеры в кучу я сделал свой шаблон, такой родной, теплый.

Изначально угадайка скрыта внутри конструкции {{c1::overcoming}}. Виднеется: описание, три примера. Вспоминаем карточку - нажимаем посмотреть ответ, по желанию можно напечатать угадайку для проверки соответствия (излишне).

На обратной стороне карточки финально мы видим угадайку! Поздравляю, наш мозг впредь ассоциирует угадайку в контексте, почти как родной язык! Тут я предпочитаю включить TTS (Text to speech), слушая итог своих плодов потешая самолюбование, благо AnkiDroid предоставляет данный функционал.
А вот и код шаблона - передняя страница карточки.
<script>document.getElementById('Deck').innerHTML="{{Deck}}".replace("::"," − ");</script> <div class=Definition><span>{{cloze:Definition}}</span></div> <div class="IMG">{{IMG}}</div> <div class=Example><span>{{cloze:Example}}</span></div> {{type:Keyword}}
Обратная страница. Внимательные заметили поле IMG которое я не использую, причина проста - практика осветлила это как ресурсно-затратное мероприятие, нерентабельно, как говорят менеджеры.
<script>document.getElementById('Deck').innerHTML="{{Deck}}".replace("::"," − ");</script> <div class="IMG">{{IMG}}</div> <div class=Definition><span>{{cloze:Definition}}</span></div> <div class=Example><span>{{cloze:Example}}</span></div> {{type:Keyword}} <!hr id=answer> {{tts en_US:Keyword}} {{tts en_US:cloze:Definition}} {{tts en_US:cloze:Example}}
Стили. Ах да, для меня было открытие что шаблоны для Anki основаны на вебе.
.card { font-family: arial; line-height: 1.75em; font-size: 18px; text-align: center; color: black; background-color: #f3f3f3; } .Deck { position: absolute; top: 7px; left: 0px; width: 100%; } #Deck { font-size: 8pt; vertical-align: top; line-height: 10pt; } .cloze { font-weight: bold; color: blue; } #typeans { padding-top: 0.5em; text-align: center; max-width: 300px; } input#typeans { border-radius: 9px } IMG { border-radius: 19px; max-height: 248px; } div span { max-width: 900px; display: inline-block; text-align: center; } .Example { font-style: italic; color: gray; font-size: 16px; } .typeBad { color: #dc322f; background-color: #ffadab; font-weight:bold; font-size: 23px; } .typeMissed, .typePass { color: #217dbe; font-weight:bold; font-size: 23px; } .typeGood { background-color: #a4dab2; color: #158d35; font-weight:bold; font-size: 23px; }
Глава 2 - фантазер, мечтатель
Казалось бы на этом всё, заполняю карточки и изучаю английский язык! Времени занимало немного но мне это показалось рутиной. В голову пришла поистине гениальная идея - потрачу ЕЩЕ БОЛЬШЕ времени на разработку приложения для генерации этих самых карточек и буду штамповать их как на конвейере!
Недолго поразмыслив взял под основу React Native, для связи с апи AnkiDroid чудесно нашел готовое решение - react-native-ankidroid, правда пришлось зафоркать модуль, захаркодить (простите хехе) подняв версии зависимостей для совместимости и немного адаптировать логику под свой кейс. Для запросов к нейросетям - genai и openai.
Далее будут ключевые фрагменты кода, остальное по ссылке в репозитории <3
Важная особенность React Native (устал от него), пришлось добавить полифил для поддержки работы openai, делается это в одну строчку кода.
import 'react-native-url-polyfill/auto';
Собственно вот сердце, связь с AnkiDroid.
export const addCard = async (deckName: string, newCard: ankiDroidCard) => { await AnkiDroid.requestPermission(); // Запрашиваем права к апи const modelName = 'English Img+cloze+native_word'; const dbDeckReference = 'com.anki.ai.decks'; const dbModelReference = 'com.anki.ai.models'; const modelFields = ['Keyword', 'IMG', 'Definition', 'Example']; const cardNames = ['Cloze 1']; const questionFormat = [questionFmt1]; const answerFormat = [answerFmt1]; const deckProperties = { name: deckName, dbReference: dbDeckReference, }; const modelProperties = { name: modelName, dbReference: dbModelReference, fields: modelFields, cardNames, questionFormat, answerFormat, css, }; const fieldOrder: (keyof ankiDroidCard)[] = ['keyword', 'img', 'definition', 'examples']; const valueFields = fieldOrder.map(field => { const value = newCard[field]; return Array.isArray(value) ? value.join('<br>') : value; }); const settings = { modelId: undefined, modelProperties: modelProperties, deckId: undefined, deckProperties: deckProperties, }; const myAnkiDeck = new AnkiDroid(settings); await myAnkiDeck.addNote(valueFields, modelFields); };
Запросы к нейросетям просты как вайбкодинг до подорожания токенов.
const geminiGetResponse = async ({ apiKey, content, model }: IGetResponse): Promise<string> => { const gemini = new GoogleGenAI({ apiKey: apiKey || '' }); const response: any = await gemini.models.generateContent({ model, contents: content, }); return response.text; }; const openAIGetResponse = async ({ apiKey, content, model }: IGetResponse): Promise<string> => { const openai = new OpenAI({ apiKey: apiKey || '' }); const response = await openai.responses.create({ model, input: content }); return response.output_text; }; const openRouterGetResponse = async ({ apiKey, content, model }: IGetResponse): Promise<string | null> => { const openai = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: apiKey || '' }); const response = await openai.chat.completions.create({ model, messages: [{ role: 'user', content }], }); return response.choices[0].message.content; };
Промпт для ИИ, даже в утилиты вынес <3
const getContentAI = (prompt: string, settings: { language: string; levelOfLanguage: string }) => ` Generate me a JSON object for the word "${prompt}". The JSON should follow this interface: { word: string; posData: [ { partOfSpeech: string; definition: string; definitionCloze: string; examples: string[]; examplesCloze: string[]; }, ]; }; Return ONLY the JSON object. Reponse in ${settings.language} language, use only ${settings.levelOfLanguage} words. Make many parts of speech, in order: nouns, verbs, adjectives, adverbs, conjunctions, prepositions, interjections, pronouns, determiners, etc. You can use any tenses. Don't make double POS, make 'noun-soft-thing, noun-wild-animal, verb-lift-hands', etc. Make 3 examples. Make the definitionCloze and examplesCloze by removing the word from the definition and examples, replacing it with '{{c1::word}}' and pay attention to the correct form of the word, also you can just add {{c1::}} in the end if sentence doesn't have the word. Always enter two double dotes and 'c1::' for cloze deletions. For example: { word: "run"; posData: [ { partOfSpeech: verb-move-fast.; definition: "To move swiftly on foot."; definitionCloze: "To move swiftly on foot.{{c1::}}"; examples: [ "I like to run in the park.", "She runs very fast.", "Why are you running away?" ]; examplesCloze: [ "I like to {{c1::run}} in the park.", "She {{c1::runs}} very fast.", "Why are you {{c1::running}} away?" ]; }, ]; }; `;
По итогу это чудо инженерной мысли заработало, более того младшие модели умудрялись не нарушать типизацию которую я так яростно указал в промпте!
{ word: string; posData: [ { partOfSpeech: string; definition: string; definitionCloze: string; examples: string[]; examplesCloze: string[]; }, ]; };
Глава 3 - хотел стать художником но рисую кнопочки
Пам парам, пам парам... Пришло время интерфейса! Я знаю что цвета которые были подобраны многим покажутся своеобразными, о вкусах не спорят!

На входе нас встречает модальное окно, вставляем наш апи ключ, никому о нем не рассказываем, это важно! Поддерживаются ключи от: openai, gemini, openrouter.

Настройки
Deck name - название колоды карточек.
Language - язык который изучаете, возможно вставить свой вариант настройки, например "Испанский пират".
Language level - уровень языка.
Font color - цвет шрифтов.
AI model - выбираем модель, все просто например openrouter/google/gemini-2.5-pro - openrouter это sdk, google/gemini-2.5-pro это модель которая используется. Настройка аналогично поддерживает индивидуальные опции, если вы захотите использовать модель вне списка.

Угадайку можно загадывать абсолютно любую! Можно выдумывать слова, нейросеть подберет ближайшее определение! Выбираем сгенерированные карточки, нажимаем Add to flashcards

На экране редактирования дается возможность финально изменить карточку (при необходимости). По готовности нажать Create card
Заключение
Одно маленькое желание переросло в маленький проект. Надеюсь что у каждого читателя будет столько же мотивации и энтузиазма. Не бросайте свои идеи, доводите как минимум до состояния MVP! Проект опенсорс - буду рад если помог людям изучать языки!
Проект - https://github.com/Deal-overcomer
Скачать релизы - https://github.com/Deal-overcomer/AnkiAI-Cards/releases
