Repetition is the mother of learning
π·πΊ Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ

You read your notes once and a week later only the heading is left. Familiar. Memory keeps what you retrieve regularly, not what you read once. So revision should be questions, not re-reading: recall it yourself and it sticks; fail to recall, look at the answer, and come back to that spot later.
Used to be you had to invent the questions by hand. Now you can feed the material to ChatGPT or NotebookLM and ask for a test. Then comes a file the bot understands: it accepts it, checks the format and walks you through the questions.
Start with the material
Anything you want to remember works: lecture notes, a PDF, an article, a video transcript, your own notes.
- NotebookLM is good when you have several sources and want no invention: it answers only from the uploaded documents and shows where the fact came from.
- ChatGPT is handier when the material is already text in the chat or needs reshaping: shorten it, split it into topics, translate it.
One file β one topic. Do not mix three lectures and half a book into a single quiz: that is impossible to revise.
The prompt
Copy it whole and paste the material at the end. If the material is large, attach it as a file and say "the material is in the attached file".
You are a teacher building a practice quiz from the material below. The material is the only source of facts: do not add anything of your own.
Write 12β20 questions that test understanding and recall rather than recognition. At least half of the questions must be button-based (inline_choice, situational_scenario) so the learner taps instead of typing. The rest are typed-answer exercises (how_to_say, text_input_translation, fill_blank, error_correction, word_order). Typed answers must be short β no more than 3β4 words.
Return ONLY valid JSON, with no markdown wrapper, no comments and no text before or after. The output must be a JSON file (not test answers or explanations) β save it as a .json file. Schema:
{
"format": "tift-quiz-v4",
"version": 4,
"language": "en",
"title": "short topic name",
"description": "one sentence about what the file tests",
"topics": [
{"id": "latin_with_underscores", "title": "Topic name"}
],
"rules_summary": [
{
"rule_id": "RULE_IN_CAPITALS",
"title": "Rule or fact",
"category": "Section",
"summary": "the rule in one or two sentences",
"formula_or_pattern": "formula or pattern, if any",
"source_ref": "where the fact comes from: file, slide, section"
}
],
"questions": [ ... ]
}
Question fields:
id β a unique string: q1, q2, q3 and so on;
exercise_type β one of: inline_choice, situational_scenario, how_to_say, text_input_translation, fill_blank, error_correction, word_order, new_word;
topic β exactly one id from topics;
difficulty β 1, 2 or 3;
prompt β the task text;
explanation β one or two sentences on why the answer is correct;
rule_reference β {"rule_id": "<id from rules_summary>", "title": "<rule title>"};
source β where the question comes from.
hint β an optional link to a public source (documentation, article, official page) that helps the learner read more; in Telegram it is shown as a hint after the answer.
The answer field depends on the type:
inline_choice: "inline_keyboard": [["option 1"], ["option 2"], ["option 3"]], "correct_answer": "an exact copy of the correct option";
situational_scenario: "options": ["option 1", "option 2", "option 3"], "correct_answer": "an exact copy of the correct option";
how_to_say, text_input_translation, fill_blank, error_correction, word_order, new_word: "expected_input": ["an accepted answer", "another accepted answer"];
Requirements:
1. expected_input lists every formulation that counts as correct: with and without the final period, with and without contractions, digits and words. Only genuinely correct variants, no filler.
2. correct_answer must be an exact copy of one of the options.
3. inline_choice has 3β4 options; the wrong ones must be plausible.
4. Do not put timecodes (00:00:00) in source or source_ref unless you are sure they are real. If you do, minutes and seconds must be 0β59.
6. ids, rule_ids and topics are Latin; the task text is in the learner's language; the answers are in the language being studied.
7. No ```json, no ellipses, no // comments. The JSON must parse as is.
8. Where possible, add a `hint` field with a link to a public source (official docs, article, Wikipedia) so the learner can read more. In Telegram the hint is shown after the answer.
Material:
[PASTE THE NOTES, TEXT OR ATTACH THE FILE HERE]
The prompt is long on purpose: a model breaks the format exactly where you give it freedom. The stricter the schema, the fewer fixes afterwards.
What you should get
The file is plain JSON. A minimal working example with three question types:
{
"format": "tift-quiz-v4",
"version": 4,
"language": "en",
"title": "Present Perfect Continuous",
"description": "Practice from the lesson 3 notes.",
"topics": [
{ "id": "tenses", "title": "Tenses" }
],
"rules_summary": [
{
"rule_id": "RULE_PPC",
"title": "Present Perfect Continuous",
"category": "Grammar",
"summary": "An action that started in the past and is still going on.",
"formula_or_pattern": "have/has + been + V-ing",
"source_ref": "notes, lesson 3"
}
],
"questions": [
{
"id": "q1",
"exercise_type": "inline_choice",
"topic": "tenses",
"difficulty": 1,
"prompt": "Choose the correct option: I ___ English for two years.",
"inline_keyboard": [["learn"], ["am learning"], ["have been learning"]],
"correct_answer": "have been learning",
"explanation": "There is 'for two years' β the action is still going on.",
"rule_reference": { "rule_id": "RULE_PPC", "title": "Present Perfect Continuous" },
"source": "notes, lesson 3"
},
{
"id": "q2",
"exercise_type": "fill_blank",
"topic": "tenses",
"difficulty": 1,
"prompt": "Fill in the blank: I have been waiting ___ 9 a.m.",
"expected_input": ["since"],
"explanation": "since marks the starting point, for marks a duration.",
"rule_reference": { "rule_id": "RULE_PPC", "title": "Present Perfect Continuous" },
"source": "notes, lesson 3"
},
{
"id": "q3",
"exercise_type": "error_correction",
"topic": "tenses",
"difficulty": 2,
"prompt": "Find and correct the mistake: I have been waiting since two hours.",
"expected_input": ["I have been waiting for two hours."],
"explanation": "since takes a starting point, for takes a duration.",
"rule_reference": { "rule_id": "RULE_PPC", "title": "Present Perfect Continuous" },
"source": "notes, lesson 3",
"hint": "https://www.grammarly.com/blog/since-vs-for/"
}
]
}
What the bot checks
Before starting, the bot reads the whole file and collects every problem at once β not one at a time. The file must have:
| Field | Requirement |
|---|---|
format |
starts with tift-quiz; for this version β tift-quiz-v4 |
questions |
a non-empty list |
id |
present on every question and not repeated |
prompt |
non-empty task text |
exercise_type |
one of the eight values above |
topic |
present and matching one of topics[].id, when topics is filled |
rule_reference.rule_id |
present and matching one of rules_summary[].rule_id, when rules_summary is filled |
| answer | the field for the type: correct_answer, expected_input or pairs |
source, source_ref |
timecodes, if any, must be real: minutes and seconds from 0 to 59 |
If you leave topics or rules_summary empty, the match check is skipped. But topic and rule_reference.rule_id still have to be non-empty. It is easier to fill both lists and keep the questions pointing at them.
How to feed the file to the bot
- Save the model's answer to a
.jsonfile, for examplequiz.json, in UTF-8. Check that it starts with{and ends with}β no ```json and no "Here is your file:". - The bot needs a Wait Input step with input type File upload. The bot pauses there and waits for a document.
- Send
quiz.jsonto the bot as a document. Sending it as a text message will not work: the step waits for a file. - The file is stored on the server, and the next node β the Uploaded quiz file is valid condition β downloads it and checks the format.
- If the format is broken, the flow takes the "no" branch and shows the list of problems on that branch's message β it takes them from
{ $user->extra.quiz_problems }. Fix everything on the list and send the file again β nothing is cached, the file is read fresh on every attempt. - If the file is valid, the flow takes the "yes" branch: the bot runs the quiz from the file and records the answers in the user's record β How the quiz is played tells how every question type is answered.
Your own node sequence looks like this:
/start β Wait Input (File upload) β Condition (Uploaded quiz file is valid)
βββ yes β quiz from the file
βββ no β "the file did not fit, send another one"
How the quiz is played
Once the file passes the check, the bot walks the learner through the questions one by one:
| Question type | How it is answered |
|---|---|
inline_choice, situational_scenario |
one button per option; the learner taps one |
how_to_say, text_input_translation, fill_blank, error_correction, word_order |
the learner types the answer |
match_pairs |
the bot shows the terms and the definitions in two lists, the definitions in a different order; the learner types every match as term - definition, one pair per line |
After every answer the bot removes the question from the chat, shows the verdict (β
Correct! or β Not quite) as a short silent message, sends the explanation as the next message and then the next question. A repeated tap on a question that is already answered is ignored, so the chat never shows the same question twice. At the end it reports the score: how many answers were correct, how many were wrong, and how many questions the file holds. The answers stay in the user's record, so a wrong one can be shown again later.
Two rules decide whether a typed answer counts:
- A text answer is compared as a whole string, ignoring the case and the spaces around it. It has to match one of the
expected_inputvariants β a correct wording that is missing from that list counts as a mistake. - A
match_pairsanswer is checked pair by pair: the lines may come in any order, the separator may be-,:or=, and the case does not matter β but every pair must be there and must match the file. After a wrong attempt the bot shows the correct mapping.
Common mistakes
- A ```json wrapper. Models love adding it "for beauty". Remove it: the file must start with
{. - Smart quotes. Curly quotes instead of
"break JSON. Replace them. - Duplicate
id. Twoq3entries and the file is rejected. Number them in order. correct_answerdoes not match an option. Even a trailing space makes the answer wrong.- Empty
expected_input. The list needs at least one non-empty string. - An impossible timecode.
00:96:00does not exist; the bot will notice. topicnot intopics. When the lists are filled, references must match exactly.
Try it yourself
Ready-made quizzes and fresh sets are posted in the RepetitionJsonQuizzes channel. Send your own file to @repetitionJsonQuizBot and it will walk you through the questions.
Why repetition works
One quiz is not an exam, it is a way to find the holes. Take it, see what you forgot, go back to those places in your notes. Take it again the next day, then in a week. Five minutes a day beats three hours before the deadline: retrieval itself strengthens the material, and the mistakes show exactly what still needs work.