Recall turns a topic or a photo of notes into flashcards. The generation function asks OpenAI for a deck title and a list of basic or cloze cards, then returns the result to the app. Getting that response into a predictable structure solves a real integration problem. Deciding whether the questions deserve a place in someone's study material takes more work.
I use a strict response schema so the app can read the cards reliably. The learner then reviews the generated deck before saving it. Neither step guarantees that the material is worth studying.
What the schema can promise
In supabase/functions/generate-cards/index.ts, I send a Chat Completions request with this response format:
response_format: {
type: "json_schema",
json_schema: { name: "deck", strict: true, schema: SCHEMA },
},The schema requires title and cards. Each card requires type, front and back; type must be basic or cloze and the two text fields must be strings. Both object levels reject additional properties. For a completed, non-refused response, those constraints give the app a defined shape to consume.
The system prompt asks for considerably more: exactly one atomic fact per card, specific questions, no yes/no questions, no pointless trivia, a mix of card types, eight to fourteen cards and a title of three to five words. None of those preferences is fully enforced by this schema. It has no array-length bounds, nonempty-string constraints, or check that a cloze question contains a blank. Allowing two type values also doesn't require the deck to contain both.
Consider this deliberately constructed card, not an observed model response:
{
"type": "cloze",
"front": "Explain this concept and give two examples.",
"back": "It depends on the context."
}It satisfies the card schema. It has no blank, no named concept and no answer someone could assess consistently. Its shape gives me no reason to keep it. OpenAI's August 2024 Structured Outputs announcement explicitly distinguishes schema adherence from mistakes inside the returned values. A constrained string can still contain a false statement.
A topic and a photo need different rules
Topic mode uses gpt-4o-mini and asks for accurate, foundational material about the supplied topic. The request contains no retrieved references, syllabus, or learner history. A broad topic leaves the model to choose the scope. Even a sound introductory deck could miss the particular distinction someone is struggling with.
That is the problem described in an August 2025 r/Anki discussion: a generated deck can contain obvious or irrelevant details, leaving the learner to decide what is worth keeping. Correct answers can still be the wrong material to study.
Photo mode uses gpt-4o, sends the image with detail: "high" and adds an instruction to use only material actually written in the image. It asks for an empty array when there is no studyable content. That is an instruction about grounding, not a verification mechanism. The function doesn't compare answers against a transcription or attach a source region to each card. A misread word could become a fluent question and answer without violating the schema.
The requested card count creates another tension. A short note may support fewer than eight distinct questions. Repeating the same fact or adding outside information to reach the requested count would undermine the other instructions. I would accept fewer useful cards from sparse notes. The current schema permits that, although the prompt still asks for eight to fourteen.
After receiving the response, the function handles a model refusal and parses the message content. Its explicit content check is small:
if (!Array.isArray(parsed.cards) || parsed.cards.length === 0) {
return json(
{
error:
"Couldn't make cards from that. Try clearer notes or a more specific topic.",
},
422,
);
}This catches an empty result. It doesn't revalidate each card or evaluate factual accuracy. The function also doesn't inspect finish_reason; if generation stops with incomplete JSON, parsing falls through to the generic error handler. Refusal, incomplete output and an unhelpful but valid deck are separate failure cases. Only some currently receive a specific response.
Generation stops at a draft
Successful generation puts the title and cards into React state and opens “Review deck.” It does not immediately create a saved deck. The learner can rename the title, inspect both sides, remove cards and explicitly save the remaining set. Removing every card disables saving. This separates the model's suggestion from the learner's collection, while keeping generation and saving as distinct actions in the interface.
The preview renders card fronts and backs as text. Content editing comes after saving, on the deck screen, where the learner can change the front, back and type, or add a missing card. That editor trims both text fields and refuses to save if either is empty. The initial generation save path has no equivalent per-card check.
That sequencing makes a useful correction awkward. If a question combines two facts, the learner must save it before rewriting it and adding the second card. I would move those editing controls into the draft review screen and share the nonempty-field validation across both paths. A cloze-specific blank check could catch another mechanical defect, while leaving relevance and truth for a different kind of review.
Make correction part of review
Alexey Rubtsov's May 2025 Medium essay on automated learning material raises a harder question: what happens to the work of choosing a concept and composing a question when a model does it? For Recall, that makes the review screen a product decision as much as an error check. It should give the learner room to change the material, rather than making acceptance the easiest action.
For photo generation, I would also keep the source visible beside the draft so a learner can check a disputed word without leaving the review. The current preview doesn't display it. Making correction possible at the moment someone spots a problem is the next concrete improvement I want in Recall.