Files
breakpilot-lehrer/backend-lehrer/services/story_generator.py
Benjamin Admin cba877c65a
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 37s
CI / test-go-edu-search (push) Successful in 35s
CI / test-python-klausur (push) Failing after 2m41s
CI / test-python-agent-core (push) Successful in 30s
CI / test-nodejs-website (push) Successful in 38s
Restructure: Move final 16 root files into packages (backend-lehrer)
classroom/ (+2): state_engine_api, state_engine_models
vocabulary/ (2): api, db
worksheets/ (2): api, models
services/  (+6): audio, email, translation, claude_vision, ai_processor, story_generator
api/        (4): school, klausur_proxy, progress, user_language

Only main.py + config.py remain at root. 16 shims added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 22:50:37 +02:00

109 lines
3.5 KiB
Python

"""
Story Generator — Creates short stories using vocabulary words.
Generates age-appropriate mini-stories (3-5 sentences) that incorporate
the given vocabulary words, marked with <mark> tags for highlighting.
Uses Ollama (local LLM) for generation.
"""
import os
import json
import logging
import requests
from typing import List, Dict, Any, Optional
logger = logging.getLogger(__name__)
OLLAMA_URL = os.getenv("OLLAMA_BASE_URL", "http://host.docker.internal:11434")
STORY_MODEL = os.getenv("STORY_MODEL", "llama3.1:8b")
def generate_story(
vocabulary: List[Dict[str, str]],
language: str = "en",
grade_level: str = "5-8",
max_words: int = 5,
) -> Dict[str, Any]:
"""
Generate a short story incorporating vocabulary words.
Args:
vocabulary: List of dicts with 'english' and 'german' keys
language: 'en' for English story, 'de' for German story
grade_level: Target grade level
max_words: Maximum vocab words to include (to keep story short)
Returns:
Dict with 'story_html', 'story_text', 'vocab_used', 'language'
"""
# Select subset of vocabulary
words = vocabulary[:max_words]
word_list = [w.get("english", "") if language == "en" else w.get("german", "") for w in words]
word_list = [w for w in word_list if w.strip()]
if not word_list:
return {"story_html": "", "story_text": "", "vocab_used": [], "language": language}
lang_name = "English" if language == "en" else "German"
words_str = ", ".join(word_list)
prompt = f"""Write a short story (3-5 sentences) in {lang_name} for a grade {grade_level} student.
The story MUST use these vocabulary words: {words_str}
Rules:
1. The story should be fun and age-appropriate
2. Each vocabulary word must appear at least once
3. Keep sentences simple and clear
4. The story should make sense and be engaging
Write ONLY the story, nothing else. No title, no introduction."""
try:
resp = requests.post(
f"{OLLAMA_URL}/api/generate",
json={
"model": STORY_MODEL,
"prompt": prompt,
"stream": False,
"options": {"temperature": 0.8, "num_predict": 300},
},
timeout=30,
)
resp.raise_for_status()
story_text = resp.json().get("response", "").strip()
except Exception as e:
logger.error(f"Story generation failed: {e}")
# Fallback: simple template story
story_text = _fallback_story(word_list, language)
# Mark vocabulary words in the story
story_html = story_text
vocab_found = []
for word in word_list:
if word.lower() in story_html.lower():
# Case-insensitive replacement preserving original case
import re
pattern = re.compile(re.escape(word), re.IGNORECASE)
story_html = pattern.sub(
lambda m: f'<mark class="vocab-highlight">{m.group()}</mark>',
story_html,
count=1,
)
vocab_found.append(word)
return {
"story_html": story_html,
"story_text": story_text,
"vocab_used": vocab_found,
"vocab_total": len(word_list),
"language": language,
}
def _fallback_story(words: List[str], language: str) -> str:
"""Simple fallback when LLM is unavailable."""
if language == "de":
return f"Heute habe ich neue Woerter gelernt: {', '.join(words)}. Es war ein guter Tag zum Lernen."
return f"Today I learned new words: {', '.join(words)}. It was a great day for learning."