loader image

LeesRovers: Hoe Technologie het Leren Van Een Taal Leuk Maakt voor Kinderen

In de bibliotheek van Lier vindt elke vrijdagmiddag iets bijzonders plaats: Lees^Rovers, een initiatief van Mondiale Werken Regio Lier (MoWe). Dit leesbevorderingsproject richt zich op kwetsbare leerlingen uit de lagere school met een taal- of leesachterstand. In kleine groepjes van maximaal vier leerlingen per begeleider wordt gewerkt aan leesplezier, woordenschat en (begrijpend) lezen. De focus ligt op het stimuleren van hardop lezen en het ontdekken van persoonlijke leesinteresses.

Om dit leerproces nog leuker en effectiever te maken, hebben Noah Van pollaert en Toon Wauters, twee studenten Toegepaste Informatica met een specialisatie in App Development, de LeesRovers-applicatie ontwikkeld. Dit project maakt deel uit van hun vak The Lab, een belangrijke voorbereiding op hun stage en de echte IT-wereld.

LeesRovers: de digitale ondersteuning voor Lees^Rovers

Hoewel de namen op elkaar lijken, is er een belangrijk verschil: Lees^Rovers is het bestaande leesbevorderingsproject van MoWe, terwijl de LeesRovers-applicatie een nieuwe digitale tool is die speciaal voor dit project is ontwikkeld.

Voorheen werd het Lees^Rovers-spel zonder technologie gespeeld, waarbij begeleiders de teksten handmatig voorlazen. Met de nieuwe webapplicatie wordt dit proces geautomatiseerd en toegankelijker, waardoor begeleiders zich nog beter kunnen richten op de begeleiding van de leerlingen.

Mondiale Werken Regio Lier (MoWe)

MoWe is een vrijwilligersorganisatie opgericht in 2016 en actief in 14 gemeenten in de provincie Antwerpen. Met de inzet van 150 vrijwilligers ondersteunt MoWe asielzoekers, vluchtelingen, daklozen en mensen in een kwetsbare positie. De organisatie werkt aan 14 verschillende projecten, waaronder woonhulp, werkhulp en formulierenhulp. Daarnaast heeft MoWe een onderwijsproject om jonge nieuwkomers te helpen bij het leren van de Nederlandse taal en het integreren in het onderwijs. Lees^Rovers is één van de initiatieven binnen dit onderwijsproject.

Noah en Toon over het project

“We vonden dit een heel fijn project omdat we zelf op onderzoek mochten gaan en veel vrijheid kregen om creatieve oplossingen te bedenken. Het was ook een geweldige ervaring om samen te werken met een externe organisatie zoals Mondiale Werken en Radua Boubouh.”

Noah en Toon over het project:
“We vonden dit een heel fijn project omdat we zelf op onderzoek mochten gaan en veel vrijheid kregen om creatieve oplossingen te bedenken. Het was ook een geweldige ervaring om samen te werken met een externe organisatie zoals Mondiale Werken en Radua Boubouh.”

Het Probleem

Kinderen met migratieachtergrond hebben vaak moeite met het leren van een nieuwe taal, vooral als ze al veel hebben meegemaakt. Het LeesRovers Project wilde deze kinderen helpen, maar stuitte op een aantal uitdagingen:

  1. Taalbarrière: Veel kinderen begrijpen complexe teksten niet.

  2. Betrokkenheid: Het was moeilijk om de kinderen gemotiveerd te houden tijdens het leren.

  3. Beperkte middelen: De vrijwilligers hadden behoefte aan een tool die hen kon ondersteunen bij het voorlezen en het spelen van educatieve spellen.

Reading with children: why is it so important? - Little Lives UKFiguur 1: Kinderen die moeite hebben met lezen. Bron: Kiwanis

De Oplossing

Om deze problemen aan te pakken, hebben we de LeesRovers-webapplicatie ontwikkeld. Deze applicatie combineert technologie en creativiteit om een interactieve en kindvriendelijke ervaring te bieden. Hier is hoe het werkt:

  1. Voorlezen van teksten: De applicatie gebruikt Google Text-to-Speech om verhalen voor te lezen.

  2. Interactief galgje-spel: Kinderen spelen een galgje-spel met woorden uit de tekst, waarbij hints worden gegeven via verwante woorden, afbeeldingen en voorgelezen hints.

  3. Visuele en interactieve elementen: Een ruimte thema met animaties maakt het leren leuk en boeiend.

Figuur 2: Werking van de LeesRovers-webapplicatie.


Technologieën en Implementatie

Architectuur van het LeesRovers-project

Het LeesRovers-project bestaat uit drie hoofdcomponenten: een frontend, een backend en een AI-service, ondersteund door een PostgreSQL-database. De frontend is gebouwd met ReactTypeScript en TailwindCSS, en biedt een interactieve gebruikersinterface met animaties en reveal-effects via Framer Motion en React Awesome Reveal. Deze laag communiceert met de backend voor data en functionaliteiten.

De backend is ontwikkeld met Spring Boot in Java en fungeert als het centrale punt voor logica en communicatie. Het verwerkt requests van de frontend, communiceert met externe services zoals Google Cloud Storage voor afbeeldingen en Google Text-to-Speech voor het voorlezen van teksten, en stuurt verzoeken door naar de AI-service. Daarnaast beheert het de gegevensopslag in de PostgreSQL-database, die in een Docker-container draait voor eenvoudig beheer.

De AI-service is een Python-applicatie gebouwd met FastAPI en Uvicorn, en draait ook in een Docker-container. Deze service gebruikt het RobBERT-v2-Dutch-Base model van Huggingface om verwante woorden te genereren voor hints in het galgje-spel. De backend stuurt requests naar deze service en ontvangt de gegenereerde hints, die vervolgens aan de frontend worden doorgegeven.

1. Google Text-to-Speech

Google Text-to-Speech is een service van Google die tekst omzet naar audio. Het ondersteunt verschillende talen en stemmen, waardoor het perfect is voor het voorlezen van verhalen. In de applicatie wordt deze technologie gebruikt om zowel teksten als hints tijdens het galgje-spel voor te lezen, wat zorgt voor een toegankelijke en interactieve ervaring.

Code Snippet (Java):

package kdg.be.bookaneers.backend.service;

import com.google.cloud.texttospeech.v1.*;
import com.google.protobuf.ByteString;
import org.springframework.stereotype.Service;
import java.io.FileOutputStream;
import java.io.OutputStream;

@Service
public class TextToSpeechService {
    public String convertTextToSpeech(String text) throws Exception {
        try (TextToSpeechClient textToSpeechClient = TextToSpeechClient.create()) {
            SynthesisInput input = SynthesisInput.newBuilder().setText(text).build();
            VoiceSelectionParams voice = VoiceSelectionParams.newBuilder()
                    .setLanguageCode("nl-NL")
                    .setSsmlGender(SsmlVoiceGender.NEUTRAL)
                    .build();
            AudioConfig audioConfig = AudioConfig.newBuilder()
                    .setAudioEncoding(AudioEncoding.MP3)
                    .build();

            SynthesizeSpeechResponse response = textToSpeechClient.synthesizeSpeech(input, voice, audioConfig);
            ByteString audioContents = response.getAudioContent();

            String filePath = "output.mp3";
            try (OutputStream out = new FileOutputStream(filePath)) {
                out.write(audioContents.toByteArray());
            }
            return filePath;
        }
    }
}


2. Huggingface en RobBERT-v2-Dutch-Base

Huggingface is een platform dat state-of-the-art NLP-modellen aanbiedt, waaronder het RobBERT-v2-Dutch-Base model. Dit model is specifiek getraind op Nederlandse tekst en wordt gebruikt om verwante woorden te genereren. In de applicatie wordt RobBERT-v2-Dutch-Base ingezet om verwante woorden te creëren voor de hints in het galgje-spel. Bijvoorbeeld, als het woord “kat” geraden moet worden, kan het model suggesties geven zoals “poes” of “huisdier”.

Hoewel RobBERT-v2-Dutch-Base uitstekend presteert bij taken zoals verwante woorden te genereren, heeft het enkele beperkingen wanneer het gaat om het genereren van teksten met een vooraf bepaalde lengte. Dit komt omdat het model ontworpen is om contextueel relevante output te produceren, maar niet altijd precies de gewenste lengte kan bereiken zonder aanvullende aanpassingen. Dit kan een uitdaging vormen bij taken zoals het genereren van korte zinnen of teksten met een specifieke lengte, zoals in sommige NLP-toepassingen vereist is.

Tijdens de ontwikkeling hebben we ook gekeken naar alternatieven zoals Geitje en Bertje, twee andere Nederlandse taalmodellen die vergelijkbaar zijn met RobBERT. Deze modellen zijn echter zwaarder en complexer, waardoor ze meer rekenkracht en geheugen vereisen. Omdat we een lightweight en efficiënte oplossing wilden, was RobBERT de betere keuze.

Daarnaast hebben we overwogen om Langchain4J te gebruiken, een Java-bibliotheek die helpt bij het communiceren met Large Language Models (LLM’s) via API’s. Hoewel Langchain4J handig kan zijn voor het integreren van LLM’s en voordelen biedt zoals eenvoudige integratie, schaalbaarheid en ondersteuning voor meerdere modellen, hebben we besloten om zelf een API te bouwen met FastAPI (Python). Dit gaf ons meer flexibiliteit en controle over de communicatie tussen de front-end en back-end van de applicatie.

 Code Snippet (Python)
from fastapi import FastAPI
from transformers import pipeline, AutoModelForMaskedLM, AutoTokenizer
import re
import csv
import os

# Laad het getrainde model en tokenizer
model = AutoModelForMaskedLM.from_pretrained("./trained_model")
tokenizer = AutoTokenizer.from_pretrained("./trained_model")

# Maak een pipeline voor invullen van maskers
mask_filler = pipeline("fill-mask", model=model, tokenizer=tokenizer)

# FastAPI-applicatie
app = FastAPI()

# Functie om verwante woorden te genereren
def generate_synonyms(word, excluded_words):
    prompt = f"Wanneer een kind een hint vraagt voor het te raden Nederlands woord in galgje, dan is het woord '<mask>' een synoniem voor '{word}' omdat het hetzelfde betekent."
    results = mask_filler(prompt)

    filtered_results = []
    for result in results:
        synonym = result["token_str"].lower()
        synonym = re.sub(r'[^a-zA-Z\u00C0-\u017F\s]', '', synonym)

        if synonym and synonym != word and synonym not in excluded_words:
            filtered_results.append(synonym)

    return filtered_results

# Functie om uitgesloten woorden uit een CSV-bestand te lezen
def read_excluded_words_from_csv(file_path):
    excluded_words = set()
    if os.path.exists(file_path):
        with open(file_path, mode='r', newline='', encoding='utf-8') as file:
            reader = csv.DictReader(file)
            for row in reader:
                excluded_words.add(row['word'].strip().lower())
    return excluded_words

# Endpoints voor FastAPI

@app.get("/api/generate-synonym/{word}")
def get_synonym(word: str):
    excluded_words = read_excluded_words_from_csv('resources/excluded_words.csv')
    synonyms = generate_synonyms(word, excluded_words)
    return {"word": word, "synonyms": synonyms}

3.Framer Motion en React Awesome Reveal voor Animaties en Reveal-Effects

Om de gebruikerservaring van de LeesRovers-webapplicatie visueel aantrekkelijk en interactief te maken, hebben we twee krachtige React-bibliotheken gebruikt: Framer Motion en React Awesome Reveal.

Framer Motion is een geavanceerde bibliotheek voor het creëren van soepele en dynamische animaties. Het biedt een eenvoudige API waarmee complexe animaties, zoals de raketanimatie, eenvoudig kunnen worden toegevoegd. Bij elk correct antwoord vliegt een raket in een boog naar een planeet, wat kinderen een gevoel van voldoening en vooruitgang geeft. Deze animaties maken de applicatie niet alleen functioneel, maar ook visueel aantrekkelijk en leuk om te gebruiken.

React Awesome Reveal voegt daarnaast subtiele maar effectieve reveal-effects toe, zoals fade-ins en geleidelijk zichtbaar wordende elementen. Deze effects zorgen voor een vloeiende overgang tussen schermen en een professionelere uitstraling. Bijvoorbeeld, knoppen of tekst worden geleidelijk zichtbaar wanneer de gebruiker door de applicatie navigeert, wat de interactiviteit en betrokkenheid vergroot.

Beide bibliotheken zijn lightweight en eenvoudig te integreren, waardoor ze perfect passen bij de behoeften van dit project. Samen zorgen ze voor een speelse, boeiende en professionele gebruikerservaring die kinderen motiveert om te leren.

Code Snippet (React):

 
TODO !!!


4. FastAPI en Uvicorn

FastAPI is een modern, snel (high-performance) web framework voor het bouwen van API’s met Python. Het staat bekend om zijn eenvoud, snelheid en ondersteuning voor asynchrone operaties. Uvicorn, een ASGI-server (Asynchronous Server Gateway Interface), wordt gebruikt om FastAPI-applicaties te draaien. Deze combinatie is lightweight en ideaal voor het hosten van snelle en schaalbare API’s.

In het project wordt FastAPI gebruikt om een API te bouwen die verwante woorden genereert met behulp van het RobBERT-v2-Dutch-Base model. De FastAPI-applicatie draait in een Docker-container, wat het deployment-proces eenvoudiger en consistenter maakt.

De voordelen van FastAPI en Uvicorn zijn onder andere snelheid, dankzij de asynchrone mogelijkheden van FastAPI, eenvoudige integratie door het gemak waarmee endpoints gedefinieerd en getest kunnen worden, en schaalbaarheid, omdat Uvicorn asynchrone requests ondersteunt. Dit zorgt ervoor dat de applicatie efficiënt kan omgaan met meerdere gelijktijdige gebruikers.

5. Zelfgemaakte Webscraper en Google Cloud Storage

Een zelfgemaakte webscraper in Java haalt afbeeldingen op van DuckDuckGo, die vervolgens worden opgeslagen in Google Cloud Storage. Deze afbeeldingen worden in de applicatie gebruikt als visuele hints tijdens het spel. Het is belangrijk om te vermelden dat webscrapen niet altijd toegestaan is, vooral wanneer een project commercieel wordt gebruikt. Omdat dit project primair gericht is op educatieve doeleinden en het opdoen van ervaring met scraping, hebben we ervoor gekozen om het op deze manier uit te werken. Mochten we het project in de toekomst willen verkopen of commercieel gebruiken, dan kunnen we overstappen op API’s die specifiek zijn ontworpen voor het ophalen van afbeeldingen, zoals de Unsplash API of de Pixabay API. Deze API’s bieden een legale en betrouwbare manier om afbeeldingen te gebruiken. De scraper zorgt nu echter voor visuele ondersteuning van de hints, wat de gebruikerservaring verrijkt.

 

Code Snippet (Java):

 
package kdg.be.bookaneers.backend.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import kdg.be.bookaneers.backend.controller.dto.SearchImageResponse;
import kdg.be.bookaneers.backend.exceptions.ImageSearchException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
public class ImageSearchService {

    @Value("${search-engine.url}")
    private String searchEngineUrl;
    @Value("${search-engine.url.image}")
    private String searchEngineImageUrl;

    private final WebClient webClient;
    private final ObjectMapper objectMapper;

    public ImageSearchService(ObjectMapper objectMapper) {
        this.webClient = WebClient.builder()
                .defaultHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
                        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36")
                .build();
        this.objectMapper = objectMapper;
    }

    public SearchImageResponse searchImages(String keyword, short amount) {
        List<String> imageUrls = new ArrayList<>();
        try {
            String vqdToken = getVqd(keyword);
            String url = searchEngineImageUrl + "?q=" + URLEncoder.encode(keyword, StandardCharsets.UTF_8)
                    + "&format=json&vqd=" + vqdToken;

            Mono<String> responseMono = webClient.get()
                    .uri(url)
                    .retrieve()
                    .bodyToMono(String.class);
            String response = responseMono.block();
            JsonNode root = objectMapper.readTree(response);
            JsonNode results = root.get("results");

            if (results != null && results.isArray()) {
                for (int i = 0; i < Math.min(amount, results.size()); i++) {
                    JsonNode result = results.get(i);
                    if (result.has("image")) {
                        String imageUrl = result.get("image").asText();
                        imageUrls.add(imageUrl);
                    }
                }
            } else {
                System.out.println("Geen resultaten gevonden in het 'results' veld.");
            }
        } catch (Exception e) {
            throw new ImageSearchException("Failed to search for images: " + e.getMessage());
        }
        return new SearchImageResponse(keyword, imageUrls);
    }

    private String getVqd(String keyword) throws Exception {
        String url = searchEngineUrl + "/?q=" + URLEncoder.encode(keyword, StandardCharsets.UTF_8);
        String html = webClient.get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class)
                .block();
        Pattern pattern = Pattern.compile("vqd\\s*=\\s*[\"']([\\d-]+)[\"']");
        if (html == null) {
            throw new Exception("Failed to fetch DuckDuckGo search page");
        }
        Matcher matcher = pattern.matcher(html);
        if (matcher.find()) {
            return matcher.group(1);
        }
        throw new Exception("Failed to extract vqd parameter");
    }
}

Ervaring in de Bibliotheek van Lier

Op vrijdag hebben we de applicatie getest in de bibliotheek van Lier. De kinderen waren meteen enthousiast over het ruimtethema en de interactieve elementen.

Figuur 3: Kinderen gebruiken de LeesRovers-webapplicatie.

Reacties:

  • “Tijdens mijn studie hebben we aan verschillende projecten gewerkt, maar bij dit project kregen we volledige vrijheid om creatief te zijn en nieuwe technologieën uit te testen. Wat me het meest is bijgebleven, is hoe de kinderen juichten wanneer ze een juiste letter hadden geraden en de raket dichter naar de planeet vloog.” – Noah Van pollaert

  • “De kinderen waren meteen enthousiast en deden met veel plezier mee. Het spel zorgde voor extra motivatie om te lezen!”  Radua Boubouh, Mondiale Werken


Conclusie en Toekomstplannen

De LeesRovers-webapplicatie is een perfect voorbeeld van hoe technologie kan worden ingezet om educatie leuker en toegankelijker te maken. Door gebruik te maken van geavanceerde tools zoals Google Text-to-Speech, Huggingface, Framer Motion en meer, hebben we een platform gecreëerd dat niet alleen effectief is, maar ook kindvriendelijk en boeiend. Omdat Mondiale Werken met beperkte middelen werkt, is de applicatie zo ontworpen dat deze kostenefficiënt kan worden gedeployed en geschaald. Hier zijn enkele mogelijkheden en tips om het project verder te optimaliseren:

Het Python FastAPI-project, dat verantwoordelijk is voor het genereren van verwante woorden met behulp van het RobBERT-v2-Dutch-Base model, draait al in een Docker-container. Dit maakt het eenvoudig om de service te deployen en te schalen. Een mogelijkheid is om een Docker Compose-bestand te gebruiken om de FastAPI-service samen met andere services, zoals de database, te beheren. Dit vereenvoudigt het deployment-proces aanzienlijk.

De backend van de applicatie is gebouwd met Spring (Java) en biedt de mogelijkheid om gecontaineriseerd te worden met behulp van een Dockerfile. Dit zou de backend geschikt maken voor deployment naar cloudplatforms zoals Google Cloud Run of AWS ECS. Ook de frontend, gebouwd met React, kan worden gecontaineriseerd en gehost op platforms zoals Netlify, Vercel of in een containerized omgeving, wat flexibiliteit en schaalbaarheid biedt.

Voor het opslaan van afbeeldingen en andere statische bestanden wordt Google Cloud Storage gebruikt. Dit is een schaalbare en kostenefficiënte oplossing, waarbij je alleen betaalt voor wat je gebruikt. Een mogelijkheid is om gebruik te maken van de gratis versie van Google Cloud, die een beperkte hoeveelheid opslag en bandbreedte gratis aanbiedt. Voor het voorlezen van teksten wordt Google Text-to-Speech gebruikt, een betaalde service waarbij je alleen betaalt per aantal karakters dat wordt omgezet naar spraak. Om de kosten verder te optimaliseren, kunnen essentiële teksten worden geselecteerd voor het voorlezen en kan caching worden toegepast om dubbele requests te voorkomen.

Een andere mogelijkheid is om de Spring-backend te hosten op een serverless platform zoals Google Cloud Run of AWS Lambda. Deze services zijn zeer kostenefficiënt omdat je alleen betaalt voor de daadwerkelijke uitvoeringstijd van je code. Voor de front-end kunnen goedkope of gratis platforms zoals GitHub Pages, Netlify of Vercel worden gebruikt, die vaak gratis versies aanbieden voor kleine projecten.

Met de LeesRovers-webapplicatie zijn er talloze mogelijkheden voor verdere ontwikkeling en uitbreiding. De applicatie ondersteunt nu al het genereren van hints in verschillende talen, en de interface kan ook in meerdere talen worden weergegeven. Daarnaast is er al een functionaliteit om teksten in verschillende talen toe te voegen, wat de applicatie geschikt maakt voor een internationale doelgroep. Om deze functionaliteit beter te beveiligen, kan er gebruik worden gemaakt van een authenticatiesysteem zoals Keycloak, wat ervoor zorgt dat alleen geautoriseerde gebruikers teksten kunnen toevoegen of aanpassen.

Met deze mogelijkheden kan de LeesRovers-webapplicatie niet alleen een duurzaam project blijven, maar ook een toegankelijke oplossing voor organisaties met beperkte middelen, zoals Mondiale Werken. Het biedt een sterke basis voor verdere innovatie en uitbreiding, waardoor het een waardevol educatief hulpmiddel kan worden voor kinderen wereldwijd.

Scroll naar boven