Лого <3
Лого <3

AnkiAI-Cards - мобильное Android приложение с помощью ИИ генерирует карточки по шаблону и отправляет их напрямую в AnkiDroid внутри смартфона. Помогает изучать иностранные слова ассоциируя с контекстом фраз и предложений.


Глава 1 - начало

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

Шаблон для карточек

Позаимствовав все готовые примеры в кучу я сделал свой шаблон, такой родной, теплый.

AnkiDroid
AnkiDroid

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

AnkiDroid
AnkiDroid

На обратной стороне карточки финально мы видим угадайку! Поздравляю, наш мозг впредь ассоциирует угадайку в контексте, почти как родной язык! Тут я предпочитаю включить TTS (Text to speech), слушая итог своих плодов потешая самолюбование, благо AnkiDroid предоставляет данный функционал.


А вот и код шаблона - передняя страница карточки.

    <script>document.getElementById('Deck').innerHTML="{{Deck}}".replace("::"," &minus; ");</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("::"," &minus; ");</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 - хотел стать художником но рисую кнопочки

Пам парам, пам парам... Пришло время интерфейса! Я знаю что цвета которые были подобраны многим покажутся своеобразными, о вкусах не спорят!

AnkiAI-Cards <3
AnkiAI-Cards <3

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

AnkiAI-Cards <3
AnkiAI-Cards <3

Настройки

Deck name - название колоды карточек.

Language - язык который изучаете, возможно вставить свой вариант настройки, например "Испанский пират".

Language level - уровень языка.

Font color - цвет шрифтов.

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

AnkiAI-Cards <3
AnkiAI-Cards <3

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

AnkiAI-Cards <3
AnkiAI-Cards <3

На экране редактирования дается возможность финально изменить карточку (при необходимости). По готовности нажать Create card


Заключение

Одно маленькое желание переросло в маленький проект. Надеюсь что у каждого читателя будет столько же мотивации и энтузиазма. Не бросайте свои идеи, доводите как минимум до состояния MVP! Проект опенсорс - буду рад если помог людям изучать языки!

Проект - https://github.com/Deal-overcomer

Скачать релизы - https://github.com/Deal-overcomer/AnkiAI-Cards/releases