diff --git a/tools/migrations/26-03-31--add_topic_suggestion_to_audio_lessons.sql b/tools/migrations/26-03-31--add_topic_suggestion_to_audio_lessons.sql new file mode 100644 index 00000000..c5c51def --- /dev/null +++ b/tools/migrations/26-03-31--add_topic_suggestion_to_audio_lessons.sql @@ -0,0 +1,9 @@ +-- Add topic_suggestion column to audio_lesson_meaning for per-topic script isolation +ALTER TABLE audio_lesson_meaning +ADD COLUMN topic_suggestion VARCHAR(100) DEFAULT NULL +COMMENT 'Optional user-provided topic hint that themed the LLM dialogue'; + +-- Add topic_suggestion column to daily_audio_lesson for display in title +ALTER TABLE daily_audio_lesson +ADD COLUMN topic_suggestion VARCHAR(100) DEFAULT NULL +COMMENT 'Optional user-provided topic hint for this lesson'; diff --git a/zeeguu/api/endpoints/audio_lessons.py b/zeeguu/api/endpoints/audio_lessons.py index 5984ecc4..4390d0cc 100644 --- a/zeeguu/api/endpoints/audio_lessons.py +++ b/zeeguu/api/endpoints/audio_lessons.py @@ -57,6 +57,7 @@ def _generate_lesson_in_background(user_id, preparation): translation_language=preparation["translation_language"], cefr_level=preparation["cefr_level"], progress=progress, + topic_suggestion=preparation.get("topic_suggestion"), ) except Exception as e: log(f"[background_generate] Error for user {user_id}: {e}") @@ -87,8 +88,9 @@ def generate_daily_lesson(): # Get timezone offset from form data (default to 0 for UTC) timezone_offset = flask.request.form.get("timezone_offset", 0, type=int) + topic_suggestion = flask.request.form.get("topic_suggestion", "").strip()[:100] or None - result = generator.prepare_lesson_generation(user, timezone_offset) + result = generator.prepare_lesson_generation(user, timezone_offset, topic_suggestion) # Existing lesson found — return it directly if result.get("lesson_id"): diff --git a/zeeguu/core/audio_lessons/daily_lesson_generator.py b/zeeguu/core/audio_lessons/daily_lesson_generator.py index c95b62e4..586e203c 100644 --- a/zeeguu/core/audio_lessons/daily_lesson_generator.py +++ b/zeeguu/core/audio_lessons/daily_lesson_generator.py @@ -42,7 +42,7 @@ def lesson_builder(self): self._lesson_builder = LessonBuilder() return self._lesson_builder - def prepare_lesson_generation(self, user, timezone_offset=0): + def prepare_lesson_generation(self, user, timezone_offset=0, topic_suggestion=None): """ Validate and prepare for lesson generation (synchronous, fast). Returns either an error/existing-lesson dict, or a preparation dict @@ -51,6 +51,7 @@ def prepare_lesson_generation(self, user, timezone_offset=0): Args: user: The User object to generate a lesson for timezone_offset: Client's timezone offset in minutes from UTC + topic_suggestion: Optional short topic hint for the LLM (max 100 chars) Returns: Dictionary with either error info, existing lesson, or preparation data @@ -130,6 +131,7 @@ def prepare_lesson_generation(self, user, timezone_offset=0): "translation_language": translation_language, "cefr_level": cefr_level, "progress_id": progress.id, + "topic_suggestion": topic_suggestion, } def select_words_for_lesson( @@ -158,6 +160,7 @@ def generate_audio_lesson_meaning( cefr_level, created_by="claude-v1", progress=None, + topic_suggestion=None, ): """ Generate an AudioLessonMeaning for a specific user word. @@ -169,6 +172,7 @@ def generate_audio_lesson_meaning( cefr_level: CEFR level for the lesson created_by: String identifying who created this lesson progress: Optional AudioLessonGenerationProgress for tracking + topic_suggestion: Optional short topic hint for the LLM Returns: AudioLessonMeaning object @@ -179,8 +183,10 @@ def generate_audio_lesson_meaning( meaning = user_word.meaning teacher_lang = Language.find_or_create(translation_language) - # Check if audio lesson already exists for this meaning and teacher language - existing_lesson = AudioLessonMeaning.find(meaning=meaning, teacher_language=teacher_lang) + # Check if audio lesson already exists for this meaning, teacher language, and topic + existing_lesson = AudioLessonMeaning.find( + meaning=meaning, teacher_language=teacher_lang, topic_suggestion=topic_suggestion + ) if existing_lesson: return existing_lesson @@ -196,6 +202,7 @@ def generate_audio_lesson_meaning( origin_language=origin_language, translation_language=translation_language, cefr_level=cefr_level, + topic_suggestion=topic_suggestion, ) # Update progress: script done @@ -210,6 +217,7 @@ def generate_audio_lesson_meaning( created_by=created_by, difficulty_level=cefr_level, teacher_language=teacher_lang, + topic_suggestion=topic_suggestion, ) db.session.add(audio_lesson_meaning) db.session.flush() # Get the ID @@ -250,6 +258,7 @@ def generate_daily_lesson( translation_language: str, cefr_level: str, progress: AudioLessonGenerationProgress = None, + topic_suggestion: str = None, ) -> dict: """ Generate a daily audio lesson for the given user with specific words. @@ -261,6 +270,7 @@ def generate_daily_lesson( origin_language: Language code for the words being learned (e.g. 'es', 'da') translation_language: Language code for translations (e.g. 'en') cefr_level: CEFR level for the lesson (e.g. 'A1', 'B2') + topic_suggestion: Optional short topic hint for the LLM Returns: Dictionary with lesson details or error information @@ -277,6 +287,7 @@ def generate_daily_lesson( user=user, created_by="generate_daily_lesson_v1", language=user.learned_language, + topic_suggestion=topic_suggestion, ) db.session.add(daily_lesson) log(f"[generate_daily_lesson] Created daily lesson object") @@ -309,6 +320,7 @@ def generate_daily_lesson( audio_lesson_meaning = self.generate_audio_lesson_meaning( user_word, origin_language, translation_language, cefr_level, progress=progress, + topic_suggestion=topic_suggestion, ) except Exception as e: log( @@ -459,6 +471,7 @@ def _format_lesson_response(self, lesson): "is_paused": lesson.is_paused, "is_completed": lesson.is_completed, "listened_count": lesson.listened_count, + "topic_suggestion": lesson.topic_suggestion, } def get_daily_lesson_for_user(self, user, lesson_id=None): diff --git a/zeeguu/core/audio_lessons/script_generator.py b/zeeguu/core/audio_lessons/script_generator.py index faa10d41..3bef1b79 100644 --- a/zeeguu/core/audio_lessons/script_generator.py +++ b/zeeguu/core/audio_lessons/script_generator.py @@ -24,6 +24,7 @@ def generate_lesson_script( translation_language: str, cefr_level: str = "A1", generator_prompt_file="meaning_lesson--teacher_challenges_both_dialogue_and_beyond-v2.txt", + topic_suggestion: str = None, ) -> str: """ Generate a lesson script using Claude API. @@ -35,6 +36,7 @@ def generate_lesson_script( translation_language: Language code of the translation (e.g., 'en') cefr_level: Cefr level of the word being learned generator_prompt_file: full filename + topic_suggestion: Optional short topic hint for the LLM Returns: Generated script text @@ -75,7 +77,10 @@ def generate_lesson_script( cefr_level=cefr_level, ) - log(f"Generating script for {origin_word} -> {translation_word}") + if topic_suggestion: + prompt += f'\nCONTEXT: Set the dialogue scenario in a context related to "{topic_suggestion}". The examples and challenges should use vocabulary relevant to this topic.\n' + + log(f"Generating script for {origin_word} -> {translation_word} (topic: {topic_suggestion})") try: # Use unified LLM service with automatic Anthropic -> DeepSeek fallback diff --git a/zeeguu/core/model/audio_lesson_meaning.py b/zeeguu/core/model/audio_lesson_meaning.py index 464f3fe8..f2a630cc 100644 --- a/zeeguu/core/model/audio_lesson_meaning.py +++ b/zeeguu/core/model/audio_lesson_meaning.py @@ -24,6 +24,7 @@ class AudioLessonMeaning(db.Model): script = Column(Text, nullable=False) voice_config = Column(JSON) + topic_suggestion = Column(String(100), nullable=True) teacher_language_id = Column(Integer, ForeignKey(Language.id), nullable=True) teacher_language = relationship(Language) @@ -45,6 +46,7 @@ def __init__( voice_config=None, duration_seconds=None, teacher_language=None, + topic_suggestion=None, ): self.meaning_id = meaning.id self.script = script @@ -53,6 +55,7 @@ def __init__( self.lesson_type = lesson_type self.voice_config = voice_config self.duration_seconds = duration_seconds + self.topic_suggestion = topic_suggestion if teacher_language: self.teacher_language_id = teacher_language.id @@ -66,9 +69,10 @@ def audio_file_path(self): return f"/audio/lessons/{self.meaning_id}-{lang_code}.mp3" @classmethod - def find(cls, meaning, teacher_language=None): - """Find audio lesson for a specific meaning and teacher language""" + def find(cls, meaning, teacher_language=None, topic_suggestion=None): + """Find audio lesson for a specific meaning, teacher language, and topic""" query = cls.query.filter_by(meaning=meaning) if teacher_language: query = query.filter_by(teacher_language_id=teacher_language.id) + query = query.filter_by(topic_suggestion=topic_suggestion) return query.first() diff --git a/zeeguu/core/model/daily_audio_lesson.py b/zeeguu/core/model/daily_audio_lesson.py index 5104a918..83706fe8 100644 --- a/zeeguu/core/model/daily_audio_lesson.py +++ b/zeeguu/core/model/daily_audio_lesson.py @@ -40,6 +40,9 @@ class DailyAudioLesson(db.Model): last_paused_at = Column(TIMESTAMP) pause_position_seconds = Column(Integer, default=0) + # Optional topic suggestion that themed the lesson + topic_suggestion = Column(db.String(100), nullable=True) + # Relationship to segments (individual meaning lessons) segments = relationship( "DailyAudioLessonSegment", @@ -48,12 +51,13 @@ class DailyAudioLesson(db.Model): cascade="all, delete-orphan", ) - def __init__(self, user, created_by, voice_config=None, duration_seconds=None, language=None): + def __init__(self, user, created_by, voice_config=None, duration_seconds=None, language=None, topic_suggestion=None): self.user = user self.created_by = created_by self.voice_config = voice_config self.duration_seconds = duration_seconds self.language = language or user.learned_language + self.topic_suggestion = topic_suggestion self.listened_count = 0 self.pause_position_seconds = 0