early_language_learning_agent is a ROS 2 (Python / ament_python) package that runs an interactive, story-based early language learning experience for preschool-aged children.
At runtime, ELLA alternates between:
- Narrative chunks: pre-generated story narration with synchronized facial expressions + body motion.
- Interaction chunks: short, child-facing questions designed to elicit and reinforce a target vocabulary word, with LLM-driven scaffolding.
This repository folder is intended to be open-sourced as the early_language_learning_package (ROS package name: early_language_learning_agent).
- Story session orchestration (wake-up → in-session chunk loop → wind-down)
- Pre-recorded narrative playback (base64 MP3 decoded to WAV for ROS playback)
- Vocabulary scaffolding via Groq-hosted LLMs (JSON-structured multi-utterance replies)
- Turn-taking using VAD + turn-end detection + ASR transcript buffering
- Robot behavior coupling (facial expressions + timed gesture primitives + body poses)
- Optional start/stop recording hooks for session capture
early_language_learning_agent/session_manager.py— top-level ROS node that runs a story sessionstory_models.py— story document + chunk schemanarrative_delivery.py— plays narrative chunk audio + animationsconversation_manager.py— runs interactive chunks (questions, ASR, scaffolding)interaction_handlers.py— per-interaction-type response generationscaffold_response_generator.py— unified scaffold + transition generationresponse_delivery.py— TTS → audio playback + expressions/gesturesturn_manager.py— VAD/turn-end driven turn completionanimation.py— scheduling utilities for face/body programsgroq_llm_provider.py— minimal Groq chat-completions client
stories/— story JSON files (often large due to embedded base64 audio)prompts/— prompt templates (scaffolds/,greeting/,goodbye/)config/config.yaml— session configurationlaunch/ella_session.launch.py— launches the full “robot stack” + ELLA session
- ROS 2 (Python
rclpy) m_interfaces(custom action/service definitions)- Python deps (installed via
setup.py):pyyamlpydubgroq
pydub typically requires FFmpeg to decode MP3:
- macOS:
brew install ffmpeg - Ubuntu:
sudo apt-get install ffmpeg
- Groq API for LLM scaffolding
- set
GROQ_API_KEY
- set
The default Groq model is currently set in code (see interaction_handlers.py / scaffold_response_generator.py), defaulting to:
openai/gpt-oss-20b
To change it, adjust the model= argument when constructing Groq_LLMProvider / handlers.
From your ROS 2 workspace:
# from <ws>
colcon build --packages-select early_language_learning_agent
source install/setup.bashIf you use rosdep, make sure dependencies like m_interfaces, audio_processing, motor_control, m_commons, and m_logging are also available in your workspace.
source /root/m_ws/.envThis launch file brings up the surrounding robot/audio stack and the ELLA session manager.
ros2 launch early_language_learning_agent ella_session.launch.py
# or choose a story
ros2 launch early_language_learning_agent ella_session.launch.py story:=quest.jsonYou can run just the session manager node, but it expects several ROS interfaces (TTS, audio playback, pose control, ASR/VAD topics) to exist:
ros2 run early_language_learning_agent session_manager --ros-args -p story_filename:=quest.jsonELLA loads config/config.yaml in this order:
ELLA_CONFIG_PATHenvironment variable- Installed package share directory (ament index)
- Source checkout (relative to the early_language_learning package)
config/config.yaml keys:
story_directory(default:"stories")prompts_directory(default:"prompts")wake_up_delay,cooldown_between_chunks,wind_down_delay(seconds)initial_wait_ms,vad_silence_timeout_ms,grace_period_ms(milliseconds)child_namemax_interaction_turnsskip_wake_up,skip_wind_down,skip_interactions
Relative story_directory / prompts_directory values are resolved relative to the installed share directory (preferred) or the source checkout.
Stories are JSON objects with:
story: full story text (often includes<interaction ...>tags for authoring)chunks: ordered list of chunk objects
Chunk types:
{
"type": "narrative",
"content": "...",
"audio": "<base64 mp3>",
"face_programs": [["{...json...}", 0.0]],
"body_programs": [["[{...timeline...}]", 0.0]]
}{
"type": "interaction",
"content": {
"interaction_type": "greeting" | "vocab" | "goodbye",
"vocab": "quest",
"question": "What do you think makes a quest different...?"
},
"audio": "<base64 mp3>"
}Notes:
audiois expected to be MP3 encoded as base64; ELLA converts it to WAV for ROS playback.face_programsandbody_programsare optional and are scheduled relative to chunk start.- The loader automatically attaches prior and upcoming narrative context to interaction chunks for better transitions.
Prompts live under:
prompts/scaffolds/scaffold.txt— unified scaffolding promptprompts/scaffolds/transition_vocab.txt— transition back into story after vocab interactionprompts/greeting/greeting.txt— greeting interactionprompts/goodbye/goodbye.txt— goodbye interaction
ELLA expects the LLM to return a JSON array. The code is tolerant, but best results come from returning:
- (optional) a first object like
{ "strategy": "<name>" } - followed by one or more utterance objects:
[
{"strategy": "eliciting"},
{"utterance": "Wow!", "expression": "excitement:high", "gesture": "beat:start"},
{"utterance": "Can you say quest?", "expression": "interested:low", "gesture": "invite:mid"}
]Each utterance is delivered via:
tts_synthesize(TextToSpeech service → MP3 base64)- MP3→WAV conversion
audio_playaction
While audio plays, ELLA publishes facial expressions and schedules gesture primitives.
asr_text(std_msgs/String) — streaming ASR text fragmentsvad_result(std_msgs/Bool) — VAD speech/not-speechturn_completion(std_msgs/String) — turn-end detector results (expectscomplete/incomplete)
session_active(std_msgs/Bool) — session lifecycle statusrobot_speaking(std_msgs/Bool) — whether the robot is currently speakingasr_enable(std_msgs/Bool) — enables/disables ASR bufferingface_expression(std_msgs/String) — face controller commands (e.g.,talking,reset)robot_utterance(std_msgs/String) — emitted for logging each spoken utterance
tts_synthesize(m_interfaces/srv/TextToSpeech)set_pose(m_interfaces/srv/SetPose)audio_play(m_interfaces/action/AudioPlay)/start_recording+/stop_recording(std_srvs/srv/SetBool) — used by the launch file / session end hook
colcon test --packages-select early_language_learning_agent
colcon test-result --verbose- FFmpeg missing: MP3→WAV conversion will fail.
- No Groq key: set
GROQ_API_KEY. - Missing ROS interfaces: running
session_managerwithout the surrounding stack will fail to synthesize/play speech.
Package metadata declares CC BY 4.0 (see package.xml / setup.py).