diff --git a/api/docs/design-articles.md b/api/docs/design-articles.md new file mode 100644 index 0000000..d48b080 --- /dev/null +++ b/api/docs/design-articles.md @@ -0,0 +1,52 @@ +# Technical Design Doc: Articles + +> You may wish to review documentation about [architecture](./architecture.md) and the [domain](./domain.md) of this application to help make sense of this document. + +An Article represents a single piece of content that a learner can read and/or listen to. It might be, for example, a 300-word fictional piece about a baker in Lyon, or it could be a 500-word summary of recent news events. + +Articles will be accompanies by a (AI-generated) text-to-speech. + +Because this is a language-learning app, Articles will be authored in one language (e.g. French) and there will be a parallel set of content in another language (e.g. English). + +Not every learner will have access to every Article at the same time. For example, learners who are studying French won't access Italian language Articles. Intermediate French learners won't access advanced or basic French language Articles. + +Because Articles can be available in Audio, Articles will also form the basis of a podcast-style RSS feed for each learner. Allowing them to _just_ listen. + +The Article is therefore the primitive of the content, but not how a learner will access, or receive, their content. There will need to be a separate piece of architecture which makes Articles available to the learner, e.g. through a daily or weekly "edition" of content from the website (similar to a newspaper) + +A separate role of Users will author, edit, and publish Articles - using a traditional CMS-like interface. Articles can therefore be in a _draft_ state, before they are published. Articles are also versioned entities, i.e. if I wish to make a change to an article, as an author, I would log in, make that change, and then click "update" or "publish", which would then kick off an async process to replace the previous article version with the new one. Primarily this is because of the audio-generation pipeline of an Article. + +## Foundational data model + +In the interest of delivering value incrementally, as opposed to "all at once", let's create the following entities: + +The Article entity is the Header that contains a reference to the content, describing the article itself: + +```json +{ + "id": "article_id", + "source_language": "fr", + "target_language": "en", + "title": "Le boulangerie", + "subtitle": null, // nullable string + "subject_tags": ["fiction", "france"], + "length_descriptor": "short" // short,medium,long, +} +``` + +And this is the "record" row of the Article, which we could call the ArticleVersion:" + +```json +{ + "id": "some-uuid", + "article_id": "article_id", + "created_at": "2026-04-22T19:00Z", + "published_at": "2026-04-24T09:00Z", // nullable, if not published, + "deleted_at": null, + "source_language_markdown_text": "This is where the article is", + "target_language_markdown_text": "voila la langue franciase", // nullable if not generated + "source_language_natural_language_data": {..}, // nullable, output from SpaCy tokensation + "target_language_natural_language_data": {..}, // nullable, output from SpaCy tokensation + "source_language_audio_url": "http://", // nullable if not generated +} +``` diff --git a/api/docs/design-doc-choose-your-own-adventure.md b/api/docs/design-doc-choose-your-own-adventure.md new file mode 100644 index 0000000..c53f01f --- /dev/null +++ b/api/docs/design-doc-choose-your-own-adventure.md @@ -0,0 +1,174 @@ +# 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" +} +```