{"id":410517,"date":"2024-06-29T21:26:33","date_gmt":"2024-06-29T21:26:33","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=410517"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=410517","title":{"rendered":"<span>Your own Duolingo without overengineering<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Hi, my name is Mikhail Emelyanov, I\u2019m a Python programmer and I would like to show you my pet project \u2014 Flywheel, a micro-platform for learning foreign languages, a mixture of Duolingo and Anki, an application that can teach you to properly write in Spanish (or any other language you\u2019re studying). Flywheel\u2019s source code is available on <a href=\"https:\/\/github.com\/amaargiru\/flywheel\" rel=\"nofollow noopener noreferrer\">GitHub<\/a>.<\/p>\n<p>  <\/p>\n<p><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/post_images\/150\/c8a\/377\/150c8a377d435f9973f4f92527c03ff2.png\" alt=\"Flywheel\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/post_images\/150\/c8a\/377\/150c8a377d435f9973f4f92527c03ff2.png\"\/><\/p>\n<p>  <\/p>\n<p>As you may know, generalized knowledge of a foreign language can be broken down into four relatively independent components: reading, writing, listening, and speaking. Unfortunately, training one of these abilities has no direct effect on the other components, so, for example, by developing our reading skills, the effect on our writing skills is quite indirect. Flywheel is a \u2018sharpener\u2019 specifically for <strong>written Spanish<\/strong>.<\/p>\n<p>  <\/p>\n<p>If you\u2019ve ever used Duolingo, you should have some idea of the format in which you\u2019ll be studying. The formula is simple: here\u2019s a phrase, translate it into the other language; the app will remember the last time you translated a phrase and how successful you were at it; and depending on the accuracy of your answer, it will determine when you should do the same phrase again. In my opinion, Duolingo and its approach are brilliant. However\u2026 There are certain aspects that somewhat spoil the learning experience, and Flywheel was specifically designed to address them.<\/p>\n<p><a name=\"habracut\"><\/a>  <\/p>\n<h3 id=\"wish-list\">Wish List<\/h3>\n<p>  <\/p>\n<p>First and most importantly, I want all translation assignments to be English to Spanish only. I only want to see English phrases that I need to translate into Spanish. I don\u2019t want to translate from Spanish to English. I\u2019m not studying to be a translator; I want to learn a foreign language! And in my opinion, the way to do that is to not write anything in English at all while I\u2019m studying. There\u2019s a little lifehack for Duolingo \u2014 you can switch from learning Spanish for English speakers to learning <em>English for Spanish speakers<\/em> (this, in part, explains the large number of students in this course) so that the course will contain more English-to-Spanish assignments, although the amount of Spanish-to-English translations will still be very large. Whereas I want 100 % of the lesson time to be written in Spanish!<\/p>\n<p>  <\/p>\n<p>Second, I\u2019m an adult, and I don\u2019t need the studying process to be gamified at all. All those little people cheerfully winking, encouraging and advising me is one giant, irrelevant and annoying pain. There are even browser extensions that try to cut out all of these unnecessary functions, reducing the website\u2019s visuals to the necessary level of minimalism.<\/p>\n<p>  <\/p>\n<p>Third, I\u2019m an adult (yes, I\u2019m repeating myself) and sometimes I don\u2019t have the time for a full-sized lesson. While Duolingo has fairly short ones, the breakdown of the learning process into set lessons containing an XX amount of questions is primarily convenient for the learning platform, not the learner. I want to be able to repeat not twenty phrases, but, say, five or three, or even one. I want to be able to interrupt the studying process at any moment without losing progress! After all, I sometimes am only able to practice on rare breaks of undetermined duration between my main activities over tea and cookies, or during breaks between spending time with the kids. If I have only a literal spare minute, I want to do a couple of sets and maintain my progress.<\/p>\n<p>  <\/p>\n<p>Fourth, I want to be able to add new phrases at any stage of my study! After hearing or reading something new, useful or just interesting, I want to add the phrase to the list, letting the app ensure that this phrase will remain in my memory forever.<\/p>\n<p>  <\/p>\n<p>Fifth, but also no less important consideration \u2014 I want the program to indicate the wrong parts of the translation. Sometimes the entered text contains small mistakes or typos, catching which is difficult with a naked eye. I want the program to show the difference between my translation and the correct version, so that I can focus on learning Spanish, and not on the game of finding the wrong letter in a long phrase in a foreign language.<\/p>\n<p>  <\/p>\n<p>This is my wish list \u2014 I want Duolingo, but only with English-to-Spanish tasks, without gamification, saving progress after each task, with the ability to add new phrases and with the visualization of the errors made, even minor ones.<\/p>\n<p>  <\/p>\n<p>I think that\u2019s where the preface can end and we can get to the heart of the matter. If you simply want to start learning Spanish, go to the next section, \u2018Usage.\u2019 If you want to see the app\u2019s inner workings, go to the \u2018How It Works\u2019 section (near the end of the article).<\/p>\n<p>  <\/p>\n<h3 id=\"usage\">Usage<\/h3>\n<p>  <\/p>\n<p>Using Flywheel is extremely simple. At the start, you have just one file, phrases.txt (the file that comes with the application contains about two thousand phrases). Inside are many pairs of phrases, separated with a double vertical line, e.g.:<\/p>\n<p>  <\/p>\n<p><em>I love you || Te quiero<\/em><\/p>\n<p>  <\/p>\n<p>If the English phrase can be correctly translated into several different Spanish phrases, a single vertical line is used to separate them:<\/p>\n<p>  <\/p>\n<p><em>I know || Lo se | Ya se | Yo s\u00e9<\/em><\/p>\n<p>  <\/p>\n<p>If there are two English phrases that can also have multiple equivalent translations, a single vertical line is also used to separate them.<\/p>\n<p>  <\/p>\n<p>Of course, you can and should add <strong>your own phrase pairs<\/strong> to phrases.txt. This is the essence of Flywheel \u2014 you don\u2019t have to memorize the dictionary, it\u2019s just a template. Adjust the content of the lessons to suit your level of proficiency; move the phrase pairs you find most useful higher up in the dictionary; add pairs related to your job. Needless to say, the shell doesn\u2019t care what language you\u2019re learning. If you wish to learn French, bien accueillir! Want to learn Aleutian? No problem. Need to learn Aleutian as a native French speaker? Easy as pie!<\/p>\n<p>  <\/p>\n<p>Please don\u2019t add single words to the dictionary! Sure, technically it\u2019s possible, but it\u2019s not particularly worthwhile from the perspective of language learning efficiency. Try adding phrases specifically, and if you want to add a specific new word to your vernacular it\u2019s better to pick up a phrase which uses it in a specific context. This way you\u2019ll not only remember the word better, but you\u2019ll more easily move it from the passive phase to the active phase, as you won\u2019t simply recognize it in a text or in speech, but will actually start applying it in writing and in speaking.<\/p>\n<p>  <\/p>\n<p>Next, simply run flywheel.py. Two more files will be added to your application folder \u2014 repetitions.json (this will record your progress and memorization of all completed phrase pairs) and user_statistics.txt (this will record the total number of exercises you have completed and will generate a general list of words you have managed to learn).<\/p>\n<p>  <\/p>\n<h3 id=\"how-it-works\">How It Works<\/h3>\n<p>  <\/p>\n<p>If you are a beginner Python developer and want to try your hand at something simple but not useless, give Flywheel a whirl. Maybe you\u2019ll be able to add some hot new features to it, and improve your Spanish while debugging it as well. Naturally, most of the methods used in the application don\u2019t need a lot of describing, so I\u2019ll focus only on the general approach and the key functions that are directly related to the analysis of user progress.<\/p>\n<p>  <\/p>\n<p>Recently I have been practicing the following method: I write a template main as if all of the application\u2019s methods have already been developed and I just need to call them. This gives you sort of a bird\u2019s-eye view of the code (even if it\u2019s more like a penguin\u2019s rather than an eagle\u2019s \ud83d\ude42 and a rough estimate of the level of effort required. This is what I ended up with:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">phrases_file_name = \"phrases.txt\" repetitions_file_name = \"repetitions.json\"  if __name__ == \"__main__\":     phrases_file_path = find_or_create_file(phrases_file_name)     repetitions_file_path = find_or_create_file(repetitions_file_name)      phrases = read_phrases(phrases_file_path)     repetitions = read_repetitions(repetitions_file_path)     can_work, error_message = data_assessment(phrases, repetitions)      if can_work:         message = merge(repetitions, phrases)         print(message)         while True:             current_phrase = determine_current_phrase(repetitions)             user_result = user_session(current_phrase)             update_repetitions(repetitions, current_phrase, user_result)             save_repetitions(repetitions_file_path, repetitions)     else:         print(error_message)         exit()<\/code><\/pre>\n<p>  <\/p>\n<p>The operating logic is roughly thus:<br \/>  \u2022 we look for phrases.txt in the project directories (lots of phrase pairs separated by a dual vertical line, see the \u2018Usage\u2019 section for details); if we can\u2019t find it, we create a blank file for future editing by the user;<br \/>  \u2022 similarly, we look for repetitions.json (progress records and memorization degrees of all complete phrase pairs); if not found, we create an empty file;<br \/>  \u2022 we create data structures from the information taken from phrases.txt and repetitions.json, and then evaluate whether we can work with given combination. If phrases.txt is not empty, then okay, we can convert phrase pairs to our internal format and transfer that information to repetitions.json. If repetitions.json is not empty, then also okay, we can work with the information we\u2019ve already accumulated. Both phrases.txt and repetitions.json being empty is not okay, we have nowhere to draw the information we need to work, so we complain about this fact to the user, let them create phrases.txt with at least some minimal content;<br \/>  \u2022 during the loop, we feed a new task to the user, picking the most relevant phrase we need at the moment from the phrase dictionary. If there are phrases that require repetition, we pick them first; if all completed tasks don\u2019t require a refresher right now, we start mixing in new phrases.<br \/>  \u2022 after each task, we update the data in repetitions.json and the user\u2019s statistics, regardless of the quality of the answer.<\/p>\n<p>  <\/p>\n<p>In the process of writing the code, I divided all the functionality into data_level (sort of the essence of the language practice itself), system_level (functionality that depends on the operating system) and ui_level (methods that determine how to interact with the user), also adding a statistics file showing the total number of attempts made by the user and containing all the Spanish and English words that they learned. The final version turned out to be about the same as the original blueprint, if only a little more spread out:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">from data_level import DataOperations as dop from system_level import FileOperations as fop from ui_level import UiOperations as uop  phrases_file_name: str = 'phrases.txt' repetitions_file_name: str = 'repetitions.json' statistics_file_name: str = 'user_statistics.txt'  if __name__ == '__main__':     phrases_file_path = fop.find_or_create_file(phrases_file_name)     repetitions_file_path = fop.find_or_create_file(repetitions_file_name)     user_statistics_file_path = fop.find_or_create_file(statistics_file_name)      phrases: dict = fop.read_phrases(phrases_file_path)     repetitions: dict = fop.read_json_from_file(repetitions_file_path)     can_work, assesment_error_message = dop.data_assessment(phrases, repetitions)      statistics: dict = fop.read_json_from_file(user_statistics_file_path)      if can_work:         is_merged, merge_message = dop.merge(phrases, repetitions)         print(merge_message)         if is_merged:             fop.save_json_to_file(repetitions_file_path, repetitions)          while True:             current_phrase: str = dop.determine_next_phrase(repetitions)             user_result, best_translation = uop.user_session(current_phrase, repetitions[current_phrase])              dop.update_repetitions(repetitions, current_phrase, user_result)             fop.save_json_to_file(repetitions_file_path, repetitions)              statistics = dop.update_statistics(statistics, current_phrase, best_translation)             fop.save_json_to_file(statistics_file_name, statistics)     else:         print(assesment_error_message)         exit()<\/code><\/pre>\n<p>  <\/p>\n<p>First we need to determine whether the user answered the given question correctly, allowing for the possible existence of several correct versions of the translation.<\/p>\n<p>  <\/p>\n<pre><code class=\"python\"># import jellyfish  def find_max_string_similarity(user_input: str, translations: str | List[str]) -> (float, str):     \"\"\"Compares user_input against each string in translations\"\"\"     max_distance: float = 0      if isinstance(translations, str):         translations = [translations]     best_translation: str = translations[0]      # Cleanup and 'compactify' user input ('I   don't know!!!?' -> 'i dont know')     user_input = DataOperations._compact(DataOperations._cleanup_user_input(user_input).lower())      # 'Compactify' translations     translations = [(t, DataOperations._compact(t.lower())) for t in translations]      for translation, compact_translation in translations:         current_distance = jellyfish.jaro_distance(user_input, compact_translation)          if current_distance > max_distance:             max_distance = current_distance             best_translation = translation      return max_distance, best_translation  def _compact(input_string: str) -> str:     \"\"\"Restrict use of all special characters and allow letters and numbers only\"\"\"     return ''.join(ch for ch in input_string if ch.isalnum() or ch == ' ')<\/code><\/pre>\n<p>  <\/p>\n<p>Inside the husk engaged in data transfer, you can see the Jaro distance calculation:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">current_distance = jellyfish.jaro_distance()<\/code><\/pre>\n<p>  <\/p>\n<p>Accordingly, there is an estimate of the accuracy of the user\u2019s answer:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">level_excellent: float = 0.99 level_good: float = 0.97 level_mediocre: float = 0.65<\/code><\/pre>\n<p>  <\/p>\n<p>Come to think of it, maybe the Levenshtein distance would be more appropriate here?<\/p>\n<p>  <\/p>\n<p>By the way, try turning this:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">user_input = DataOperations._compact(DataOperations._cleanup_user_input(user_input).lower())<\/code><\/pre>\n<p>  <\/p>\n<p>into something like this (I don&#8217;t mean dropping DataOperations, but rather arranging a pipe for methods like string):<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">user_input = user_input.lower().cleanup().compact()<\/code><\/pre>\n<p>  <\/p>\n<p>Unfortunately, adding your own methods to those provided by Python requires either using subclasses or reinventing something like forbiddenfruit (bit dead already) \/ fishhook (still a little raw). Meanwhile, C# provides this feature out of the box, curses!<\/p>\n<p>  <\/p>\n<p>The interval repetition algorithm, which, depending on the quality of the answer, decides when a completed phrase will be offered to the user next time, is based on <a href=\"https:\/\/en.wikipedia.org\/wiki\/SuperMemo#Description_of_SM-2_algorithm\" rel=\"nofollow noopener noreferrer\">SuperMemo-2<\/a>:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">def _supermemo2(repetition: dict, user_result: float) -> dict:     \"\"\"Update next attempt time based on user result\"\"\"     if user_result >= DataOperations.level_good:  # Correct response         if repetition['repetition_number'] == 0:  # + 1 day             repetition['time_to_repeat'] = (datetime.now() + timedelta(days=1)).strftime(datetime_format)         elif repetition['repetition_number'] == 1:  # + 6 days             repetition['time_to_repeat'] = (datetime.now() + timedelta(days=6)).strftime(datetime_format)         else:  # + (6 * easiness_factor) days             repetition['time_to_repeat'] = (datetime.now()                                             + timedelta(days=6 * repetition['easiness_factor'])).strftime(datetime_format)         repetition['repetition_number'] += 1     else:  # Incorrect response         repetition['repetition_number'] = 0      repetition['easiness_factor'] = repetition['easiness_factor'] + (             0.1 - (5 - 5 * user_result) * (0.08 + (5 - 5 * user_result) * 0.02))     repetition['easiness_factor'] = max(repetition['easiness_factor'], 1.3)      return repetition<\/code><\/pre>\n<p>  <\/p>\n<p>The SuperMemo family of algorithms has more recent implementations, up to SuperMemo-18. You can move over to using them, repetitions.json stores the last few user attempts specifically for this purpose.<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">max_attempts_len: int = 10  # Limit for 'Attempts' list<\/code><\/pre>\n<p>  <\/p>\n<p>While you\u2019re at it, try to figure out why, despite the fact that SuperMemo-18 exists, SuperMemo-2 is still actively used, and even the most adventurous developers don\u2019t venture beyond SuperMemo-5 or, at most, a simplified SuperMemo-8. Have a look at <a href=\"https:\/\/github.com\/duolingo\/halflife-regression\/blob\/master\/settles.acl16.pdf\" rel=\"nofollow noopener noreferrer\">A Trainable Spaced Repetition Model for Language Learning<\/a>, an algorithm published by the developers of Duolingo, which attempts to address the shortcomings of previous approaches. Try to replicate Duolingo\u2019s key functionality, it\u2019s quite feasible.<\/p>\n<p>  <\/p>\n<p>Next comes the saving of the results; I think there\u2019s no need to dwell on the implementation of this function.<\/p>\n<p>  <\/p>\n<p>Now that the user\u2019s answer has been weighed and accounted for, we need to show the student not only the correct option, but also the specifics that will help them identify the mistakes. To do this, we will first form a data structure containing information on the difference between the desired and the actual result.<\/p>\n<p>  <\/p>\n<pre><code class=\"python\"># from dataclasses import dataclass # from difflib import SequenceMatcher  def find_user_mistakes(user_input: str, reference: str) -> list:     \"\"\"Dig for user errors and typos\"\"\"      @dataclass     class ComplexPhrase:         phrase_without_punctuation: List[str]         transformation_matrix: List[int]      user_input = DataOperations._cleanup_user_input(user_input).lower()     reference = reference.lower()     correction_map: list[bool] = [True] * len(reference)      complex_reference: ComplexPhrase = ComplexPhrase(phrase_without_punctuation=[], transformation_matrix=[])      # 'Minify' reference phrase and remember transformation shifts     for i, ch in enumerate(reference):         if ch.isalnum() or ch == ' ':             complex_reference.phrase_without_punctuation.append(ch)             complex_reference.transformation_matrix.append(i)      minified_reference: str = ''.join(complex_reference.phrase_without_punctuation)     corr_map: list[bool] = [False] * len(minified_reference)      # Compare cleaned user input and 'minified' reference     seq = SequenceMatcher(lambda ch: not (ch.isalnum() or ch == ' '), user_input, minified_reference)     blocks = seq.get_matching_blocks()     blocks = blocks[:-1]  # Last element is a dummy      for _, i, n in blocks:         if n >= 3:  # Don't show to the user too short groups of correct letters, perhaps he entered a completely different phrase             for x in range(i, i + n):                 corr_map[x] = True      # 'Unminify' reference phrase and restore transformation shifts     for i, corr in enumerate(corr_map):         if corr is False:             correction_map[complex_reference.transformation_matrix[i]] = False      return correction_map<\/code><\/pre>\n<p>  <\/p>\n<p>A bit complicated? At a glance, we could have taken a shorter route by directly applying SequenceMatcher to the user\u2019s response and reference phrase, like this.<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">def find_user_mistakes(user_input: str, reference: str) -> list:     \"\"\"Display of user errors\"\"\"     seq = SequenceMatcher(None,                             \"\".join(DataOperations._compact(DataOperations._cleanup_user_input(user_input).lower())),                             DataOperations._compact(reference.lower()))     blocks = seq.get_matching_blocks()     blocks = blocks[:-1]  # Last element is a dummy      corr_map: list = [False] * len(reference)      for _, i, n in blocks:         if n >= 3:  # Don't show to the user too short groups of correct letters, perhaps he entered a completely different word             for x in range(i, i + n):                 corr_map[x] = True      return corr_map<\/code><\/pre>\n<p>  <\/p>\n<p>Instead, we wrap and then unwrap some additional data structure that does not store all the characters from the source text, but remembers which characters are shifted where. What for?<\/p>\n<p>  <\/p>\n<p>The thing is, one of Duolingo\u2019s key features is that it ignores punctuation and the difference between uppercase and lowercase letters. For example, it\u2019s perfectly acceptable to type \u2018hello my name is kitty\u2019 instead of \u2018Hello! My name is Kitty,\u2019 and that\u2019s pretty cool. After all, we\u2019re primarily studying the grammar of a foreign language, having already learned the general rules of writing names and punctuation (although Spanish has its own peculiarities), and getting a fail for spelling the name Michael with a lowercase letter would certainly be a huge drawback for the whole user experience.<\/p>\n<p>  <\/p>\n<p>This is the kind of goodie I wanted to implement in Flywheel as well. That\u2019s why the reference phrase and the user\u2019s answer are first converted into plain text without punctuation and capital letters, then compared, ending with the reference phrase once again unfolded into a full response and shown to the user.<\/p>\n<p>  <\/p>\n<p>Next, to clearly show the mistakes and typos to the user, we form a full-colour user output, a phrase in which the colour of the character will depend on the correctness of its spelling:<\/p>\n<p>  <\/p>\n<pre><code class=\"python\">def _print_colored_diff(correction, reference) -> None:     \"\"\"Visualisation of user errors\"\"\"     for i, ch in enumerate(reference):         if correction[i]:             print(Fore.GREEN + ch, end='')         else:             if ch != ' ':                 print(Fore.RED + ch, end='')  # Just a letter             else:                 if i - 1 >= 0 and i + 1 &lt; len(reference):  # Emphasise the space between correct but sticky characters                     if correction[i - 1] and correction[i + 1]:                         print(Fore.RED + '_', end='')                     else:                         print(Fore.RED + ' ', end='')<\/code><\/pre>\n<p>  <\/p>\n<p>This ends the life cycle of the question in the console application.<\/p>\n<p>  <\/p>\n<p>Want something like that, but more sophisticated (because making the user quit the application using Ctrl-C is kind of gross), with a web interface, database, ORM, API, and voice prompts? Have a look in the <a href=\"https:\/\/github.com\/amaargiru\/flywheel\/tree\/main\/Legacy\" rel=\"nofollow noopener noreferrer\">flywheel\/Legacy<\/a> folder. It contains some working code that differs from the latest micro-version described in this article by having a less consistent data_level (in particular, not knowing about SuperMemo, I tried to invent my own algorithm of interval repetitions), but it has all of the aforementioned goodies. Perhaps you\u2019ll hear the quiet one-handed clap calling you back to the console later\u2026 Meanwhile, you can try to make your own startup, building a potential rival to Duolingo, Cerego, Course Hero or Memrise.<\/p>\n<p>  <\/p>\n<h3 id=\"outro\">Outro<\/h3>\n<p>  <\/p>\n<p>Well, that\u2019s about it for now. From now until the end of your current lifecycle, you can spend as much time on learning a foreign language as you like, add new phrases or add to existing translations and keep up with your progress even after minuscule efforts.<\/p>\n<p>  <\/p>\n<p>However, keep in mind that:<br \/>  \u2022 first of all, miracles are not real, and you will have to spend a considerable amount of time (<a href=\"https:\/\/support.cambridgeenglish.org\/hc\/en-gb\/articles\/202838506-Guided-learning-hours\" rel=\"nofollow noopener noreferrer\">approximate estimates<\/a>) to learn the language in any case;<br \/>  \u2022 and, secondly, as aptly noted by Ilya Frank, \u2018Language is akin to an icy hill \u2014 you have got to move fast if you want to get to the top of it,\u2019 that is, in other words, if you don\u2019t dedicate enough time to language learning, and keep to a fairly tight schedule, you will not be able to reach a new equilibrium point, and your acquired knowledge will slowly but surely fade away.<\/p>\n<p>  <\/p>\n<p>If you have any questions, feel free to leave them in the comments. As a reminder, Flywheel\u2019s source code is available on <a href=\"https:\/\/github.com\/amaargiru\/flywheel\" rel=\"nofollow noopener noreferrer\">GitHub<\/a> and is updated and corrected whenever possible. If this rather simple but, in my opinion, very effective method of learning Spanish grabbed your attention, please create repository forks, make corrections both to code (project is written in Python and contains only about four hundred lines) and to the list of translated phrases. If you could leave a star on GitHub, that would be great.<\/p>\n<p>  <\/p>\n<p>You know what I like most about this method? After a few days of using the app, my Spanish obviously didn\u2019t improve much. However! I gained a distinct feeling of control over the process of learning a foreign language! Previously, when using Duolingo, I had this feeling of passivity, like a passenger in a bumper car welded to the base of an amusement park ride: the car would move, then suddenly jerk to the right, then make a gentle left turn\u2026 Perhaps the trajectory was fairly good, and scientifically sound, but my issue was that it didn\u2019t consider my previous knowledge and individual preferences. Now that both data and methods of their processing are in my hands, I feel that my little car is more or less obeying the steering wheel and is going in the direction I need.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/744074\/\"> https:\/\/habr.com\/ru\/articles\/744074\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-1\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Hi, my name is Mikhail Emelyanov, I\u2019m a Python programmer and I would like to show you my pet project \u2014 Flywheel, a micro-platform for learning foreign languages, a mixture of Duolingo and Anki, an application that can teach you to properly write in Spanish (or any other language you\u2019re studying). Flywheel\u2019s source code is available on <a href=\"https:\/\/github.com\/amaargiru\/flywheel\" rel=\"nofollow noopener noreferrer\">GitHub<\/a>.<\/p>\n<p>  <\/p>\n<p><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/post_images\/150\/c8a\/377\/150c8a377d435f9973f4f92527c03ff2.png\" alt=\"Flywheel\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/post_images\/150\/c8a\/377\/150c8a377d435f9973f4f92527c03ff2.png\"\/><\/p>\n<p>  <\/p>\n<p>As you may know, generalized knowledge of a foreign language can be broken down into four relatively independent components: reading, writing, listening, and speaking. Unfortunately, training one of these abilities has no direct effect on the other components, so, for example, by developing our reading skills, the effect on our writing skills is quite indirect. Flywheel is a \u2018sharpener\u2019 specifically for <strong>written Spanish<\/strong>.<\/p>\n<p>  <\/p>\n<p>If you\u2019ve ever used Duolingo, you should have some idea of the format in which you\u2019ll be studying. The formula is simple: here\u2019s a phrase, translate it into the other language; the app will remember the last time you translated a phrase and how successful you were at it; and depending on the accuracy of your answer, it will determine when you should do the same phrase again. In my opinion, Duolingo and its approach are brilliant. However\u2026 There are certain aspects that somewhat spoil the learning experience, and Flywheel was specifically designed to address them.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-410517","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/410517","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=410517"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/410517\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=410517"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=410517"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=410517"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}