├── test ├── .gitignore ├── greek.txt ├── rilke.txt ├── greek.csv └── rilke.csv ├── .github └── workflows │ └── test.yaml ├── LICENSE ├── poetry.py ├── sequence.py └── README.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | output.csv -------------------------------------------------------------------------------- /test/greek.txt: -------------------------------------------------------------------------------- 1 | Greek Alphabet 2 | Alpha 3 | Beta 4 | Gamma 5 | -------------------------------------------------------------------------------- /test/rilke.txt: -------------------------------------------------------------------------------- 1 | Archaic Torso of Apollo 2 | Rainer Maria Rilke 3 | We cannot know his legendary head 4 | with eyes like ripening fruit. And yet his torso 5 | is still suffused with brilliance from inside, 6 | like a lamp, in which his gaze, now turned to low, 7 | gleams in all its power. Otherwise 8 | the curved breast could not dazzle you so, nor could 9 | a smile run through the placid hips and thighs 10 | to that dark center where procreation flared. 11 | Otherwise this stone would seem defaced 12 | beneath the translucent cascade of the shoulders 13 | and would not glisten like a wild beast's fur: 14 | would not, from all the borders of itself, 15 | burst like a star: for here there is no place 16 | that does not see you. You must change your life. -------------------------------------------------------------------------------- /test/greek.csv: -------------------------------------------------------------------------------- 1 | "# Greek Alphabet 2 | 3 | Recall all elements of the sequence. 4 | --- 5 | 1. Alpha 6 | 1. Beta 7 | 1. Gamma" 8 | "# Greek Alphabet 9 | 10 | Elements of the sequence. 11 | --- 12 | 1. {{1::Alpha}} 13 | 1. {{2::Beta}} 14 | 1. {{3::Gamma}}" 15 | "# Greek Alphabet 16 | 17 | What element has position 1? 18 | --- 19 | Alpha" 20 | "# Greek Alphabet 21 | 22 | What element has position 2? 23 | --- 24 | Beta" 25 | "# Greek Alphabet 26 | 27 | What element has position 3? 28 | --- 29 | Gamma" 30 | "# Greek Alphabet 31 | 32 | What is the position of: Alpha 33 | --- 34 | 1" 35 | "# Greek Alphabet 36 | 37 | What is the position of: Beta 38 | --- 39 | 2" 40 | "# Greek Alphabet 41 | 42 | What is the position of: Gamma 43 | --- 44 | 3" 45 | "# Greek Alphabet 46 | 47 | What comes after: Alpha 48 | --- 49 | Beta" 50 | "# Greek Alphabet 51 | 52 | What comes after: Beta 53 | --- 54 | Gamma" 55 | "# Greek Alphabet 56 | 57 | What comes before: Beta 58 | --- 59 | Alpha" 60 | "# Greek Alphabet 61 | 62 | What comes before: Gamma 63 | --- 64 | Beta" 65 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | - push 4 | jobs: 5 | build: 6 | strategy: 7 | fail-fast: true 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.x 17 | 18 | - name: Run sequence.py 19 | run: | 20 | cat greek.txt | python ../sequence.py > output.csv 21 | working-directory: test 22 | 23 | - name: Check sequence.py output 24 | run: | 25 | cmp --silent output.csv greek.csv || (echo "Files output.csv and greek.csv differ" && exit 1) 26 | working-directory: test 27 | 28 | - name: Run poetry.py 29 | run: | 30 | cat rilke.txt | python ../poetry.py > output.csv 31 | working-directory: test 32 | 33 | - name: Check poetry.py output 34 | run: | 35 | cmp --silent output.csv rilke.csv || (echo "Files output.csv and rilke.csv differ" && exit 1) 36 | working-directory: test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Fernando Borretti 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /poetry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from dataclasses import dataclass 3 | import sys 4 | import csv 5 | 6 | @dataclass(frozen=True) 7 | class Line: 8 | # The line's position in the poem, zero-indexed. 9 | index: int 10 | # The text of the poem's line. 11 | text: str 12 | 13 | def __post_init__(self): 14 | assert self.index >= 0 15 | 16 | @dataclass(frozen=True) 17 | class Card: 18 | context1: str 19 | context2: str 20 | line: str 21 | 22 | def main(): 23 | # Read all lines in the input. 24 | lines: list[str] = [line.strip() for line in sys.stdin.readlines()] 25 | # The title is the first line. 26 | title: str = lines[0].strip() 27 | # The author is the second line. 28 | author: str = lines[1].strip() 29 | # All subsequent lines are the poem. 30 | poem: list[Line] = [ 31 | Line(index=index, text=text) 32 | for index, text in enumerate(lines[2:]) 33 | ] 34 | # Make the cards. 35 | cards: list[Card] = [make_card(line, poem) for line in poem] 36 | # Render cards. 37 | writer = csv.writer( 38 | sys.stdout, 39 | delimiter=",", 40 | quotechar='"', 41 | quoting=csv.QUOTE_ALL, 42 | lineterminator="\n", 43 | ) 44 | for card in cards: 45 | writer.writerow([render_card(title, author, card)]) 46 | 47 | def make_card(line: Line, all_lines: list[Line]) -> Card: 48 | context1: str 49 | context2: str 50 | if line.index == 0: 51 | # First line of the poem. 52 | context1 = "" 53 | context2 = "_Beginning_" 54 | elif line.index == 1: 55 | # Second line of the poem. 56 | context1 = "_Beginning_" 57 | context2 = all_lines[0].text 58 | else: 59 | context1 = all_lines[line.index-2].text 60 | context2 = all_lines[line.index-1].text 61 | 62 | return Card( 63 | context1=context1, 64 | context2=context2, 65 | line=line.text, 66 | ) 67 | 68 | def render_card(title: str, author: str, card: Card) -> str: 69 | return f"# {title}\n{author}\n{card.context1}\n{card.context2}\n{{{{1::{card.line}}}}}" 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /test/rilke.csv: -------------------------------------------------------------------------------- 1 | "# Archaic Torso of Apollo 2 | Rainer Maria Rilke 3 | 4 | _Beginning_ 5 | {{1::We cannot know his legendary head}}" 6 | "# Archaic Torso of Apollo 7 | Rainer Maria Rilke 8 | _Beginning_ 9 | We cannot know his legendary head 10 | {{1::with eyes like ripening fruit. And yet his torso}}" 11 | "# Archaic Torso of Apollo 12 | Rainer Maria Rilke 13 | We cannot know his legendary head 14 | with eyes like ripening fruit. And yet his torso 15 | {{1::is still suffused with brilliance from inside,}}" 16 | "# Archaic Torso of Apollo 17 | Rainer Maria Rilke 18 | with eyes like ripening fruit. And yet his torso 19 | is still suffused with brilliance from inside, 20 | {{1::like a lamp, in which his gaze, now turned to low,}}" 21 | "# Archaic Torso of Apollo 22 | Rainer Maria Rilke 23 | is still suffused with brilliance from inside, 24 | like a lamp, in which his gaze, now turned to low, 25 | {{1::gleams in all its power. Otherwise}}" 26 | "# Archaic Torso of Apollo 27 | Rainer Maria Rilke 28 | like a lamp, in which his gaze, now turned to low, 29 | gleams in all its power. Otherwise 30 | {{1::the curved breast could not dazzle you so, nor could}}" 31 | "# Archaic Torso of Apollo 32 | Rainer Maria Rilke 33 | gleams in all its power. Otherwise 34 | the curved breast could not dazzle you so, nor could 35 | {{1::a smile run through the placid hips and thighs}}" 36 | "# Archaic Torso of Apollo 37 | Rainer Maria Rilke 38 | the curved breast could not dazzle you so, nor could 39 | a smile run through the placid hips and thighs 40 | {{1::to that dark center where procreation flared.}}" 41 | "# Archaic Torso of Apollo 42 | Rainer Maria Rilke 43 | a smile run through the placid hips and thighs 44 | to that dark center where procreation flared. 45 | {{1::Otherwise this stone would seem defaced}}" 46 | "# Archaic Torso of Apollo 47 | Rainer Maria Rilke 48 | to that dark center where procreation flared. 49 | Otherwise this stone would seem defaced 50 | {{1::beneath the translucent cascade of the shoulders}}" 51 | "# Archaic Torso of Apollo 52 | Rainer Maria Rilke 53 | Otherwise this stone would seem defaced 54 | beneath the translucent cascade of the shoulders 55 | {{1::and would not glisten like a wild beast's fur:}}" 56 | "# Archaic Torso of Apollo 57 | Rainer Maria Rilke 58 | beneath the translucent cascade of the shoulders 59 | and would not glisten like a wild beast's fur: 60 | {{1::would not, from all the borders of itself,}}" 61 | "# Archaic Torso of Apollo 62 | Rainer Maria Rilke 63 | and would not glisten like a wild beast's fur: 64 | would not, from all the borders of itself, 65 | {{1::burst like a star: for here there is no place}}" 66 | "# Archaic Torso of Apollo 67 | Rainer Maria Rilke 68 | would not, from all the borders of itself, 69 | burst like a star: for here there is no place 70 | {{1::that does not see you. You must change your life.}}" 71 | -------------------------------------------------------------------------------- /sequence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import csv 3 | import sys 4 | from dataclasses import dataclass 5 | 6 | # Types. 7 | 8 | 9 | @dataclass(frozen=True) 10 | class Elem: 11 | """ 12 | An element in the sequence. 13 | """ 14 | 15 | # Indices are humanized: the first card has index 1. 16 | index: int 17 | text: str 18 | 19 | def __post_init__(self): 20 | assert self.index > 0 21 | 22 | 23 | @dataclass(frozen=True) 24 | class Card: 25 | """ 26 | A flashcard. 27 | """ 28 | 29 | text: str 30 | 31 | 32 | # Main loop. 33 | 34 | 35 | def main(): 36 | # Read all lines from stdin. 37 | lines: list[str] = [line.strip() for line in sys.stdin.readlines()] 38 | # The first line is the title of the sequence. 39 | title: str = lines[0] 40 | # The remaining lines are the elements of the sequence. 41 | elems: list[Elem] = [ 42 | Elem(index=index + 1, text=text) for index, text in enumerate(lines[1:]) 43 | ] 44 | # Accumulator for generated cards. 45 | cards: list[Card] = [] 46 | # Make the test card. This asks us to recall the entire sequence from 47 | # beginning to end. 48 | cards.append(make_test_card(elems)) 49 | # Make the cloze card. This has the entire sequence, with each item having a 50 | # separate cloze deletion. 51 | cards.append(make_cloze_card(elems)) 52 | # The forward cards ask you to recall the element for a given index. 53 | cards += make_forward(elems) 54 | # The backwarrd cards ask you to recall the index for a given position. 55 | cards += make_backward(elems) 56 | # The successor cards ask you to remember what element comes after each. 57 | cards += make_successor(elems) 58 | # The predecessor cards ask you to remember what element comes before each. 59 | cards += make_predecessor(elems) 60 | # Render cards. 61 | writer = csv.writer( 62 | sys.stdout, 63 | delimiter=",", 64 | quotechar='"', 65 | quoting=csv.QUOTE_ALL, 66 | lineterminator="\n", 67 | ) 68 | for card in cards: 69 | writer.writerow([render_card(card, title)]) 70 | 71 | 72 | def make_test_card(elems: list[Elem]) -> Card: 73 | lst: str = "\n".join("1. " + elem.text for elem in elems) 74 | text: str = f"Recall all elements of the sequence.\n---\n{lst}" 75 | return Card(text=text) 76 | 77 | 78 | def make_cloze_card(elems: list[Elem]) -> Card: 79 | lst: str = "\n".join( 80 | "1. {{" + str(idx + 1) + "::" + elem.text + "}}" 81 | for idx, elem in enumerate(elems) 82 | ) 83 | text: str = f"Elements of the sequence.\n---\n{lst}" 84 | return Card(text=text) 85 | 86 | 87 | def render_card(card: Card, title: str) -> str: 88 | return f"# {title}\n\n{card.text}" 89 | 90 | 91 | def make_forward(elems: list[Elem]) -> list[Card]: 92 | cards: list[Card] = [] 93 | for elem in elems: 94 | text: str = f"What element has position {elem.index}?\n---\n{elem.text}" 95 | cards.append(Card(text=text)) 96 | return cards 97 | 98 | 99 | def make_backward(elems: list[Elem]) -> list[Card]: 100 | cards: list[Card] = [] 101 | for elem in elems: 102 | text: str = f"What is the position of: {elem.text}\n---\n{elem.index}" 103 | cards.append(Card(text=text)) 104 | return cards 105 | 106 | 107 | def make_successor(elems: list[Elem]) -> list[Card]: 108 | cards: list[Card] = [] 109 | for pos, elem in enumerate(elems): 110 | nxt: int = pos + 1 111 | if nxt < len(elems): 112 | succ: Elem = elems[nxt] 113 | text: str = f"What comes after: {elem.text}\n---\n{succ.text}" 114 | cards.append(Card(text=text)) 115 | return cards 116 | 117 | 118 | def make_predecessor(elems: list[Elem]) -> list[Card]: 119 | cards: list[Card] = [] 120 | for pos, elem in enumerate(elems): 121 | prev: int = pos - 1 122 | if prev >= 0: 123 | succ: Elem = elems[prev] 124 | text: str = f"What comes before: {elem.text}\n---\n{succ.text}" 125 | cards.append(Card(text=text)) 126 | return cards 127 | 128 | 129 | # Entrypoint. 130 | 131 | if __name__ == "__main__": 132 | main() 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spaced-repetition-tools ![](https://github.com/eudoxia0/spaced-repetition-tools/actions/workflows/test.yaml/badge.svg) 2 | 3 | This repository contains scripts for generating flashcards for import into 4 | [Mochi][mochi]. Most of these are based on [Gwern's scripts][gwern]. 5 | 6 | [mochi]: https://mochi.cards/ 7 | [gwern]: https://gwern.net/spaced-repetition 8 | 9 | ## Notes 10 | 11 | All scripts read from stdin and write their output as a CSV to stdout. 12 | 13 | You will want to import these into a deck with no set template, since some of 14 | the flashcards have two-sides (question-answer) and some of them have one side 15 | (cloze deletions). 16 | 17 | ## sequence.py 18 | 19 | ### Synopsis 20 | 21 | Given a sequence, this script generates flashcards to remember that sequence. The cards are: 22 | 23 | 1. A **test card** that asks you to recall the entire sequence. 24 | 1. A **cloze card** that asks you to fill one element of the sequence. 25 | 1. For each element of the sequence: 26 | 1. A **forward card** that asks you to recall the element from its position. 27 | 1. A **backward card** that asks you to recall the position of a given element. 28 | 1. A **successor card** that asks you what comes after a specific element. 29 | 1. A **predecessor card** that asks you what comes before a specific element. 30 | 31 | ### Usage 32 | 33 | ``` 34 | cat sequence.txt | ./sequence.py > output.csv 35 | ``` 36 | 37 | ### Format 38 | 39 | The input is plain text. The first line is the title of the sequence, the 40 | subsequent lines are the elements. 41 | 42 | ### Example 43 | 44 | Given a `greek.txt` file like this: 45 | 46 | ``` 47 | Greek Alphabet 48 | Alpha 49 | Beta 50 | Gamma 51 | ``` 52 | 53 | This script will generate these flashcards: 54 | 55 | | Question | Answer | 56 | |---------------------------------------------------------|---------------------| 57 | | **Greek Alphabet:** Recall all elements of the sequence | Alpha, Beta, Gamma. | 58 | | **Greek Alphabet:** What element has position 1? | Alpha. | 59 | | **Greek Alphabet:** What element has position 2? | Beta. | 60 | | **Greek Alphabet:** What element has position 3? | Gamma. | 61 | | **Greek Alphabet:** What is the position of Alpha? | 1. | 62 | | **Greek Alphabet:** What is the position of Beta? | 2. | 63 | | **Greek Alphabet:** What is the position of Gamma? | 3. | 64 | | **Greek Alphabet:** What comes after Alpha? | Beta. | 65 | | **Greek Alphabet:** What comes after Beta? | Gamma. | 66 | | **Greek Alphabet:** What comes before Beta? | Alpha. | 67 | | **Greek Alphabet:** What comes before Gamma? | Beta. | 68 | 69 | Plus the cloze card: 70 | 71 | | Cloze | 72 | |-------------------------------------------------------------------------------| 73 | | **Greek Alphabet:** Elements of the sequence: {{Alpha}}, {{Beta}}, {{Gamma}}. | 74 | 75 | ## poetry.py 76 | 77 | ### Synopsis 78 | 79 | Given a poem, this script generates flashcards where you are given some context 80 | (the previous two lines) and have to recall the next line. 81 | 82 | ### Usage 83 | 84 | ``` 85 | cat poem.txt | ./poetry.py > output.csv 86 | ``` 87 | 88 | ### Format 89 | 90 | The input is plain text. The first line is the title of the sequence, the second 91 | line is the author, and subsequent lines are the poem. 92 | 93 | ### Example 94 | 95 | Given a `wasteland.txt` file like this: 96 | 97 | ``` 98 | Archaic Torso of Apollo 99 | Rainer Maria Rilke 100 | We cannot know his legendary head 101 | with eyes like ripening fruit. And yet his torso 102 | is still suffused with brilliance from inside, 103 | like a lamp, in which his gaze, now turned to low, 104 | ... 105 | ``` 106 | 107 | This script will generate these flashcards: 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 122 | 125 | 126 | 127 | 132 | 135 | 136 | 137 | 142 | 145 | 146 | 147 | 152 | 155 | 156 | 157 |
QuestionAnswer
119 | Beginning
120 | ... 121 |
123 | We cannot know his legendary head 124 |
128 | Beginning
129 | We cannot know his legendary head
130 | ... 131 |
133 | with eyes like ripening fruit. And yet his torso 134 |
138 | We cannot know his legendary head
139 | with eyes like ripening fruit. And yet his torso
140 | ... 141 |
143 | is still suffused with brilliance from inside, 144 |
148 | with eyes like ripening fruit. And yet his torso
149 | is still suffused with brilliance from inside,
150 | ... 151 |
153 | like a lamp, in which his gaze, now turned to low, 154 |
158 | 159 | And so on. 160 | 161 | 162 | ## Mochi Import 163 | 164 | 1. Create a new deck (don't set a template). 165 | 2. Click import, CSV, double quote as the quote character. 166 | 3. Find the output file. 167 | 4. Select the deck you just created. 168 | 169 | ## License 170 | 171 | Copyright (c) 2023 [Fernando Borretti](https://borretti.me/). 172 | 173 | Released under the MIT license. 174 | --------------------------------------------------------------------------------