# Feature design doc: Choose your own adventure This is a semi-technical design document to detail the *Choose Your Own Adventure* functionality of the Langauge Learning App. ## Purpose Improve learner familiarity with a foreign language by exposing them to content generated in that language. The Choose Your Own Adventure format is chosen because it is fictitious (not all content should be non-fiction, or for "learning"). They are also engaging in that they require a little input from the user, and can be guided (at a high level) by the learner. ## Feature Description In the website there is a tab, or page, called "Adventures". On this page, learners are ablet to see any completed `adventures` (Adventures have a target number of `entries`, let's default to 6) - an adventure is *complete* when the target number of `entries` has been reached for it. When a learner creates an Adventure, they select a handful of details to aid the generation: the genre of story they want (e.g. crime fiction), the setting they want it to be in (i.e. roughly when and where), the vibes of the story (e.g. "cosy" or "thriller"), and lastly the protagonist (i.e. gender, age, one characteristic). An LLM is then used to generate the first entry in the adventure, which will introduce the learner to the story, and their character. After this has been received, we ask an LLM to create a name and a description for the adventure based on what comes back from this first entry. An Adventure now has its name, description, and some lose content tags. Each entry that gets created will receive a translation of the *Story Text* from the learner's target language, into their source language. This allows for parallel reading of the text. In time, as well, we will do natural language processing on source and target in an attempt to match sentence for sentence, or word for word, to create a better way to do the parallel reading. Each entry will also have text-to-speech done (by AI), and this can be read through the user interface, but also in the future this will allow for a per-adventure podcast feed to be generated for the learner so they can learn on the go. At the end of the entry is a set of next steps, or options, avaiable to the user. Initially there will be 4. The learner will chose one, which will repeat the cycle above (generate entry, translate, do text-to-speech, learner views it, etc.) Once the learner has run through this cycle until they have reached the target number of entries, the last entry will not have any next-step options to generate. Initially the learner will have to go and create a new Adventure, however, in the future it should be possible for them to go back to a branching point in the narrative and re-continue. If the learner is reading the adventure through the LLA's own web UI there should be a way to quick-create flashcards, and/or add words to the learner's vocabulary / word list, identifying them as words they had to look up, and perhaps as words they want to learn in the future. Additionally, over time, it would be good to generate another set of data (likely also from LLMs) that does key entity extraction from the text, and prevents stories from continually taking place in the same place, with similarly named characters. This would then be fet into the generation / system prompt, e.g. "avoid characters called Detective Renoir, avoid Paris,avoid the early 1950s" to create a variety of content. ## Monetisation and payment strategy See the [pricing.md](./design-doc-pricing.md) doc for more info. The use of LLMs creates a cost on Language Learning App per entry that is generated (initial generation, translation, text-to-speech). This will likely be as high as 50-60p per adventure, per user this could add up to a lot of money. Users who wish to operate on the subscription model will get a certain number of Adventure entries per subscription period. We should round this up to the nearest adventure (you don't want to be waiting for your next renewal to finsih an adventure). Users on a metered billing will pay for a whole adventure up-front, i.e. aprox. $1.20/adventure. For this reason, it's very important that the system tracks the costs (in money, and in tokens) taken to generate the content for an adventure, so these figures can be adjusted to reflect reality. ## Example prompts ```txt You are an experienced tabletop game master running a single-player one-shot campaign in a "choose your own adventure" format. You are helping the player learn French. Your writing respects their intelligence, avoids too many cliches, delivers satisfying plot beats, and reads naturally. The session is 8 turns. Each turn: you write a story passage, then offer 4 numbered choices. The player replies with their choice; you continue accordingly. By turn 8 there needs to be a clear end. As the player's choices reveal their character, weave those details back into the story. Don't railroad them until at least turn 4 Rules: - Write entirely in French at B1 level. No markdown — plaintext only. - Your response should be in three parts, each separated by a newline, and then five hyphens ("-----"). - The first section contains the story entry, 600–700 words length total, speaking to the player directly. - The second section contains contains a list of new-line separated player options, labelled 1,2,3,4 with explaining text. - The third section are GM notes, hidden from the player, you may optionally use this section to record notes to your future self, to keep track of threads or ideas. If no notes, simply say "no notes" - Your first message must establish: who the player is, the setting, and the broad direction of the story. - No sexual content or graphic violence. Romance, threat, and adventure are fine. Treat this as a 12-certificate. The scenario follows. ``` ## Entities ### choose_your_own_adventure This is the "header" entry, it represnts a single "adventure" in the format, right now it's linked to one user (via `user_id`) and holds details about the language and proficiency it's in, as a record of what was selected at the time. The title is `Untitled adventure` and the description is empty when it gets created, but a separate call to an LLM will create a name and a description to put here. ```json { "id": "unique-uuid", "user_id": "user-uuid", "language": "fr", "competencies": ["B1"], "max_entry_length": 8, "entry_story_text_target_length": { "min": 700, "max": 800}, "title": "Untitled adventure", "description": null, "plot_summary": null, "genres": ["crime fiction"], "setting": ["France", "city"], "vibes": ["dark", "light humour"], "protagonist": ["male", "reluctant", "late-teens"], "created_at": "2026-05-03T09:00Z", "deleted_at": null, } ``` ### choose_your_own_adventure_entry An entry is like a "turn" in a tabletop roleplaying game, or a chapter in a choose your own adventure book. These are generated one at a time, in response to user choices (the first one is generated immediately after creation of the Adventure itself). They are generated by an LLM using a prompt. They are immediately translated (via DeepL) and have text-to-speech (via Google Gemini) from the story_text content. Recording the `entry_index` and the `generated_from_possible_choice_id` allows us to model multiple replays of a specific adventure (e.g. "go back to step 3, and choose a different option to what I initially chose). ```json { "id": "uuid", "choose_your_own_adventure_id": "unique-uuid", "generated_from_possible_choice_id": "choose_your_own_adventure_entry_possible_choice-uuid", // null on entry 0 "llm_data": { "provider": "anthropic", "model": "claude-4.6" }, // JSONB for arbitrary data "entry_index": "1", // "story_text": "You find yourself in a big, dark woods...", "gamemaster_notes": "The player is playing cautiously...", // Hidden from the user "created_at": "2026-05-03T09:05", } ``` ### choose_your_own_adventure_entry_translation This represents a translation of the generated story_text into the user's native language, to help them do parallel reading between the two texts. ```json { "id": "uuid", "entry_id": "choose-your-own-adventure-entry-uuid", "component_type": "story_text", "target_language": "en", "translated_text": "This is the translated text from the entry.story_text" } ``` ### choose_your_own_adventure_entry_audio This is a text-to-speech (AI) generation of the story text, to make the content available to the user as e.g. a podcast feed, and also available on the screen. ```json { "id": "uuid", "entry_id": "choose-your-own-adventure-entry-uuid", "component_type": "story_text", "tts_provider": "google_gemini", "tts_options": { "voice": "voice name"}, // JSONB format "file_name": "uuid-like-filename.mp4" } ``` ### choose_your_own_adventure_entry_possible_choice This represents the options available to the user a the end of a specific entry, the LLM will generate 4 of them (initially). ```json { "id": "uuid", "entry_id": "choose-your-own-adventure-entry-uuid", "index": 0, "label": "1", "text": "Go into the dark house" } ``` ### choose_your_own_adventure_entry_possible_choice_decision This represents the possible_choice that a user chose, which will be used to generate the next step of the story. ```json { "id": "uuid", "choice_id": "choose_your_own_adventure_entry_possible_choice-uuid", "user_id": "user-uuid", "created_at": "2026-05-03T10:00:00.000Z" } ```