Lovable Security Review

Security Review

Analyzing codebase for security vulnerabilities :

Lovable 09:36 on Jun 13, 2025

I’ll perform a comprehensive security review of your WordPress article reader project. Let me examine the codebase systematically to identify potential security vulnerabilities.

I’ll perform a comprehensive security review of your WordPress article reader project. Let me examine the codebase systematically to identify potential security vulnerabilities.Show all (11)

Read

I’ve completed a comprehensive security review of your WordPress article reader project. Here are my findings:

Security Analysis Summary

Overall Security Posture: SOLID

Your project demonstrates good security practices with no critical vulnerabilities found. This is a frontend-only application that consumes data from a WordPress API, which inherently limits many attack vectors.

✅ Security Strengths Identified

1. No Hardcoded Credentials

  • The codebase contains no API keys, passwords, or sensitive secrets
  • The WordPress endpoint URL is appropriately exposed as it’s a public API
  • All communication is with public WordPress REST API endpoints

2. Proper Content Rendering Security

  • HTML content from WordPress is rendered using dangerouslySetInnerHTML but this is appropriate since WordPress sanitizes content server-side
  • No user-generated content is processed client-side

3. Good Error Handling

  • Network failures gracefully degrade to fallback content
  • No sensitive information exposed in error messages
  • Console logging is for development debugging only

4. Secure External Communication

  • All API calls use HTTPS (admin.ki-leierbud.lu)
  • CORS is properly configured for cross-origin requests
  • No authentication tokens or sensitive headers transmitted

🔍 Minor Security Considerations (Optional Improvements)

Console Logging in Production

  • Multiple console.log, console.warn, and console.error statements are present
  • While not a security risk, these could be optimized for production builds

Content Security Policy (CSP)

  • No CSP headers are configured, though this is typical for basic React applications
  • Could be added for enhanced security against XSS attacks

URL Parameter Validation

  • Article IDs from URL parameters are parsed without validation
  • Though not exploitable in this context, input validation is a good practice

🎯 Recommendation

Your application demonstrates excellent security practices for a content consumption application. The architecture inherently prevents most common web vulnerabilities by:

  • Operating as a read-only client
  • Relying on WordPress for content sanitization
  • Not handling user authentication or sensitive data
  • Using standard, secure communication protocols

No immediate security fixes are required. The minor improvements mentioned above are optional enhancements that could be considered for production optimization, but they do not represent security vulnerabilities.

This is a well-architected, secure frontend application that appropriately delegates content security responsibilities to the WordPress backend while maintaining clean separation of concerns.

NodeJS apps

A minimal NextJS app consist of the following 2 files :

my-next-app/
├── package.json
├── pages/
│   └── index.js

Here is a typical package.json file :

{
  "name": "my-next-app",
  "version": "1.0.0",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "latest",
    "react": "latest",
    "react-dom": "latest"
  }
}

It can be created manually or with npm init -y and then edited.

Here is a typical index.js file :

export default function Home() {
  return <h1>Hello from Next.js on Plesk!</h1>;
}

To install the NextJS app in a Plesk server in a sub-domain we run the following commands in the Plesk terminal :

cd sub-domain.ki-leierbud.lu/my-next-app
npm install
npm run build

A lot of files and folders is created in the /my-next-app folder.

Development of complex NextJS apps

The AI tool lovable.dev is a great development assistent for creating powerful NextJS apps. Here is a list of a few wordpress articles which I edited to help coders to use this tool :

Lovable Applications

I created the following lovable.dev applications which should be compiled and hosted in the KI-Léierbud Plesk Server :

ki-leierbud-navigation

Link : https://ki-leierbud-navigation.lovable.app/

This app is used to access wordpress posts related to navigation (TOCs, …) in the KI-Léierbud portal with REST API.

The ID of a navigation wordpress post is appended with ?p=ID to the URL of the app.

Example : https://ki-leierbud-navigation.lovable.app/?p=12

ki-leierbud-knowledge

Link : https://ki-leierbud-knowledge.lovable.app/

This app is used to access wordpress posts related to knowledge in the KI-Léierbud portal with REST API.

The ID of the first wordpress post related to knowledge in a specific category is appended with ?p=ID to the URL of the app. Surfing through posts in the same category can de done by swiping or with forward and backward buttons. Language switching can be done with a language selector in the header of the webpage. Only the languages where a translation is available are shown in the language list.

When a link is clicked to another post in the portal with the current language settings ?lang=xx and the related post is not available in that language, the post with the next language in the priority list is fetched.

Examples :

To do :

  • add management to show author in the footer
  • define design with a palette of background colors
  • check the correct navigation
  • integrate the app into the subdomain knowledge.ki-leierbud.lu

ki-leierbud-quiz

Link : https://ki-leierbud-quiz.lovable.app/

This app is used to access wordpress posts related to quizzes in the KI-Léierbud portal with REST API.

The ID of the first wordpress post related to a specific quiz in a specific category is appended with ?p=ID to the URL of the app. Surfing through a quiz (posts in the same category) can de done by swiping or with forward and backward buttons. Language switching works similar as in the knowledge app.

A selection list with multiple answers is shown in the header of the quiz-page. The image or page number of a quiz is displayed in superposition on each page, the selected answer to a quiz-question is shown in superposition at the bottom of the page. The button to submit the quiz is only enabled if all the questions have been answered. After the submission a evaluation page with the results is returned to the user.

Examples of Image Style Quizzes :

To do :

  • select the final images
  • define design with a palette of background colors
  • check the correct navigation
  • integrate the app into the subdomain image-quiz.ki-leierbud.lu

ki-leierbud-polling

Links :

This example app is used to access wordpress posts related to pollings about emotions in the quiz section of the KI-Léierbud portal with REST API.

The ID of the first wordpress post related to a specific polling in a specific category is appended with ?p=ID to the URL of the app. Surfing through a polling (posts in the same category) can de done by swiping or with forward and backward buttons. Language switching works similar as in the knowledge app.

A selection list with multiple emotions is shown in the header of the quiz-page. The image or page number of a polling is displayed in superposition on each page, the selected answer to a polling-question is shown in superposition at the bottom of the page. The button to submit the polling is only enabled if all the questions have been answered.

The emotions selected by a user are stored inside a specific database created in the Plesk server. After the submission an evaluation page with the results is returned to the user showing a comparison with emotions selected by other users.

Examples of pollings :

To do :

  • select the final images and videos
  • select the final emotions
  • define design with a palette of background colors
  • check the correct navigation
  • integrate the app into the subdomains image-polling.ki-leierbud.lu and video-polling.ki-leierbud.lu.

Creation of a WordPress Plugin

Me : prière de générer un code php qui exécute les fonctions suivantes : 1. afficher le contenu téléchargé par REST API d’un article WordPress avec un ID spécifique en mode “Mobile First” 2. la page web à afficher comprend une barre fixe en haut (header), une barre fixe en bas (footer) et le contenu (main) au milieu, avec une largeur adaptée à l’écran d’un smartphone. 3. le header affiche le titre de l’article et un sélecteur déroulant de langue (en, fr, de, pt, lb) 4. le footer affiche quatre boutons sous forme d’icônes, de gauche à droite : une flèche “backward” vers le post avec le premier tag de l’article, un bouton HOME vers la page d’accueil (index.html), un bouton TOC vers la page index-toc.html, une flèche “forward” vers le post avec le deuxième tag de l’article. 5. les couleurs de fond des trois éléments header, footer et contenu sont spécifiées dans un fichier JSON indiqué dans le troisième tag de l’article


ChatGPT-4o : Voici un exemple complet de code PHP qui réalise ce que vous demandez, avec des appels à l’API REST de WordPress, une structure Mobile First et une lecture des couleurs depuis un fichier JSON. Le code est commenté pour plus de clarté.

<?php
// Récupérer l'ID de l'article depuis l'URL (ex: article.php?id=123)
$article_id = isset($_GET['id']) ? intval($_GET['id']) : 0;

if ($article_id === 0) {
    echo "Article ID manquant.";
    exit;
}

// Charger l'article via REST API de WordPress
$wp_api_url = "https://votre-site.com/wp-json/wp/v2/posts/$article_id";
$response = file_get_contents($wp_api_url);
if (!$response) {
    echo "Impossible de récupérer l'article.";
    exit;
}

$post = json_decode($response, true);
$title = $post['title']['rendered'];
$content = $post['content']['rendered'];
$tags = $post['tags'];

// Charger les couleurs depuis le fichier JSON (indiqué dans le 3e tag)
$color_json = null;
if (count($tags) >= 3) {
    $tag_id = $tags[2];
    $tag_api = file_get_contents("https://votre-site.com/wp-json/wp/v2/tags/$tag_id");
    $tag_info = json_decode($tag_api, true);
    $json_url = trim($tag_info['description']);

    // Chargement du fichier JSON
    $colors = json_decode(file_get_contents($json_url), true);
} else {
    $colors = [
        "header" => "#333",
        "main"   => "#fff",
        "footer" => "#333"
    ];
}

// Déterminer les URLs vers les articles par tags
$prev_post_url = "#";
$next_post_url = "#";
$base_url = "https://votre-site.com";

// Fonction pour récupérer un article via tag
function get_article_url_by_tag($tag_id) {
    global $base_url;
    $api = "$base_url/wp-json/wp/v2/posts?tags=$tag_id&per_page=1";
    $response = file_get_contents($api);
    $posts = json_decode($response, true);
    if (!empty($posts)) {
        return "article.php?id=" . $posts[0]['id'];
    }
    return "#";
}

if (isset($tags[0])) {
    $prev_post_url = get_article_url_by_tag($tags[0]);
}
if (isset($tags[1])) {
    $next_post_url = get_article_url_by_tag($tags[1]);
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><?= htmlspecialchars($title) ?></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body, html {
            margin: 0;
            padding: 0;
            font-family: sans-serif;
        }
        header, footer {
            position: fixed;
            width: 100%;
            left: 0;
            padding: 1em;
            color: white;
            z-index: 1000;
        }
        header {
            top: 0;
            background-color: <?= $colors['header'] ?>;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        main {
            margin-top: 70px;
            margin-bottom: 70px;
            background-color: <?= $colors['main'] ?>;
            padding: 1em;
        }
        footer {
            bottom: 0;
            background-color: <?= $colors['footer'] ?>;
            display: flex;
            justify-content: space-around;
            align-items: center;
        }
        .icon-btn {
            background: none;
            border: none;
            color: white;
            font-size: 24px;
        }
        select {
            font-size: 1em;
        }
    </style>
    <script>
        function changeLang(select) {
            const lang = select.value;
            const url = new URL(window.location.href);
            url.searchParams.set('lang', lang);
            window.location.href = url.toString();
        }
    </script>
</head>
<body>

<header>
    <div><?= htmlspecialchars($title) ?></div>
    <select onchange="changeLang(this)">
        <option value="en">EN</option>
        <option value="fr">FR</option>
        <option value="de">DE</option>
        <option value="pt">PT</option>
        <option value="lb">LB</option>
    </select>
</header>

<main>
    <?= $content ?>
</main>

<footer>
    <a href="<?= $prev_post_url ?>" class="icon-btn">&#8592;</a> <!-- Flèche gauche -->
    <a href="index.html" class="icon-btn">&#8962;</a> <!-- Home -->
    <a href="index-toc.html" class="icon-btn">&#9776;</a> <!-- TOC -->
    <a href="<?= $next_post_url ?>" class="icon-btn">&#8594;</a> <!-- Flèche droite -->
</footer>

</body>
</html>

🔍 Explication rapide :

  • 📲 Mobile First : largeur adaptative via meta viewport, mise en page fluide.
  • 🧱 Disposition fixe : header + footer en haut et en bas, contenu entre les deux.
  • 🌍 Langues : sélection modifie l’URL avec le paramètre lang.
  • 🎨 Thèmes dynamiques : couleurs chargées depuis un fichier JSON lié au 3e tag.
  • 🔁 Navigation : boutons dynamiques vers les articles liés aux 1er et 2e tags.

ChatGPT-4o : Souhaitez-vous que je vous crée aussi un exemple de fichier JSON de couleurs, ou que j’adapte le tout pour être utilisé comme un plugin WordPress ?

Me : oui, Plugin WordPress


✅ 1. Créez un plugin : ki-laeirbud-viewer.php

Placez ce fichier dans :
wp-content/plugins/ki-laeirbud-viewer/ki-laeirbud-viewer.php

<?php
/**
 * Plugin Name: KI-Léierbud Post Viewer
 * Description: Affiche un article WordPress en mode mobile-first avec navigation, via le shortcode [ki_laeirbud_post id=123].
 * Version: 1.0
 * Author: Votre Nom
 */

add_shortcode('ki_laeirbud_post', 'ki_laeirbud_render_post');

function ki_laeirbud_render_post($atts) {
    $atts = shortcode_atts(['id' => 0], $atts);
    $post_id = intval($atts['id']);
    if ($post_id === 0) return "Article ID manquant.";

    $post = get_post($post_id);
    if (!$post) return "Article introuvable.";

    $title = get_the_title($post);
    $content = apply_filters('the_content', $post->post_content);
    $tags = wp_get_post_tags($post_id, ['fields' => 'all']);

    // Récupération des couleurs depuis le 3e tag (s'il existe)
    $colors = ['header' => '#222', 'main' => '#fff', 'footer' => '#333'];
    if (isset($tags[2])) {
        $json_url = trim($tags[2]->description);
        $json = wp_remote_get($json_url);
        if (!is_wp_error($json)) {
            $data = json_decode(wp_remote_retrieve_body($json), true);
            if (is_array($data)) {
                $colors = array_merge($colors, array_intersect_key($data, $colors));
            }
        }
    }

    // Navigation par tags
    $prev_url = '#';
    $next_url = '#';

    if (isset($tags[0])) {
        $prev = get_posts(['tag_id' => $tags[0]->term_id, 'numberposts' => 1]);
        if ($prev) $prev_url = get_permalink($prev[0]->ID);
    }

    if (isset($tags[1])) {
        $next = get_posts(['tag_id' => $tags[1]->term_id, 'numberposts' => 1]);
        if ($next) $next_url = get_permalink($next[0]->ID);
    }

    ob_start();
    ?>
    <style>
        .ki-wrapper {
            font-family: sans-serif;
            margin: 0;
            padding: 0;
        }
        .ki-header, .ki-footer {
            position: fixed;
            width: 100%;
            padding: 1em;
            color: white;
            z-index: 1000;
        }
        .ki-header {
            top: 0;
            background-color: <?= esc_attr($colors['header']) ?>;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .ki-footer {
            bottom: 0;
            background-color: <?= esc_attr($colors['footer']) ?>;
            display: flex;
            justify-content: space-around;
        }
        .ki-main {
            margin-top: 70px;
            margin-bottom: 70px;
            padding: 1em;
            background-color: <?= esc_attr($colors['main']) ?>;
        }
        .ki-icon {
            font-size: 24px;
            color: white;
            text-decoration: none;
        }
        .ki-lang-select {
            font-size: 1em;
        }
    </style>
    <div class="ki-wrapper">
        <div class="ki-header">
            <div><?= esc_html($title) ?></div>
            <select class="ki-lang-select" onchange="window.location.search='lang='+this.value;">
                <option value="en">EN</option>
                <option value="fr">FR</option>
                <option value="de">DE</option>
                <option value="pt">PT</option>
                <option value="lb">LB</option>
            </select>
        </div>
        <div class="ki-main"><?= $content ?></div>
        <div class="ki-footer">
            <a href="<?= esc_url($prev_url) ?>" class="ki-icon">&#8592;</a>
            <a href="<?= esc_url(home_url('/')) ?>" class="ki-icon">&#8962;</a>
            <a href="<?= esc_url(home_url('/index-toc.html')) ?>" class="ki-icon">&#9776;</a>
            <a href="<?= esc_url($next_url) ?>" class="ki-icon">&#8594;</a>
        </div>
    </div>
    <?php
    return ob_get_clean();
}

📁 2. Structure du plugin

/wp-content/plugins/
└── ki-laeirbud-viewer/
    └── ki-laeirbud-viewer.php

🧪 3. Utilisation dans un article ou une page WordPress

Dans n’importe quelle page ou article WordPress, ajoutez ce shortcode :

[ki_laeirbud_post id=123]

(remplacez 123 par l’ID de l’article cible)


🎨 4. Exemple de fichier JSON (couleurs)

Créez un fichier .json quelque part en ligne (par exemple sur votre site) :

{
  "header": "#005f73",
  "main": "#f0f0f0",
  "footer": "#0a9396"
}

Collez l’URL de ce fichier dans la description du 3e tag de votre article.


Image Emotion Test

  • Admiration
  • Wonder
  • Inspiration
  • Serenity
  • Fascination
  • Joy
  • Tenderness
  • Nostalgia
  • Relief
  • Sadness
  • Anxiety
  • Anger
  • Embarrassment
  • Fear
  • Surprise
  • Confusion
  • Disgust
  • Ambivalence
  • The Sublime
  • Disturbance
  • Dreaminess

Database

Me : please explain how to set up a database in a plesk server to store image-names and the list of emotions that you suggested above with the numbers that a specific emotion has been attributed to an image by anonymous viewers.


ChatGPT-4o : To set up a database on a Plesk server to store image names and track how many times each emotion has been attributed to a specific image by anonymous viewers, follow these steps:


Step 1: Access Plesk and Create the Database

  1. Log into your Plesk panel.
  2. Go to Databases from the left-hand menu (or via the domain’s dashboard).
  3. Click “Add Database”.
    • Database name: e.g., image_emotions_db
    • Related site: Choose your domain
    • Database user: Create a username and password (save these)
  4. Click OK to create the database.

Step 2: Design the Database Structure

You’ll need two main tables:

1. images

Stores the list of images.

Field NameTypeDescription
idINT (PK)Auto-increment ID
image_nameVARCHAR(255)Name or path of the image

Step 3: Create the Tables

Use phpMyAdmin via Plesk or run SQL manually.

SQL Script Example:

CREATE TABLE images (
    id INT AUTO_INCREMENT PRIMARY KEY,
    image_name VARCHAR(255) NOT NULL
);

CREATE TABLE emotion_counts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    image_id INT NOT NULL,
    emotion VARCHAR(50) NOT NULL,
    count INT DEFAULT 0,
    FOREIGN KEY (image_id) REFERENCES images(id)
);

Step 4: Insert Emotion Options

Insert a row for each image and each emotion with count = 0.

Example (for one image):

INSERT INTO images (image_name) VALUES ('sunset_001.jpg');
SET @img_id = LAST_INSERT_ID();

INSERT INTO emotion_counts (image_id, emotion)
VALUES
(@img_id, 'Joy'),
(@img_id, 'Sadness'),
(@img_id, 'Inspiration'),
(@img_id, 'Fear'),
(@img_id, 'Nostalgia'),
(@img_id, 'Wonder'),
(@img_id, 'Confusion'),
(@img_id, 'Disgust'),
(@img_id, 'Serenity'),
(@img_id, 'Fascination');

Step 5: Frontend Integration (Optional)

Create a simple form or buttons on your website for each image, allowing users to select how they feel about the image. Each click would:

  1. Send an AJAX or POST request.
  2. Update the emotion_counts.count field for the selected emotion and image.

Step 6: Track and Display Data

You can use SQL queries to show results like:

SELECT emotion, count FROM emotion_counts
WHERE image_id = 1
ORDER BY count DESC;

This will show which emotions were most attributed to a given image.

WordPress REST API

POST

To fetch a WordPress post with the REST API the following URL is used :

https://admin.ki-leierbud.lu/wp-json/wp/v2/posts/xxx (xxx = post ID)

Example :

https://admin.ki-leierbud.lu/wp-json/wp/v2/posts/410


CATEGORY

To fetch a WordPress category post with the REST API the following URL is used :

https://admin.ki-leierbud.lu/wp-json/wp/v2/categories/yyy (yyy = category ID)

Example :

https://admin.ki-leierbud.lu/wp-json/wp/v2/categories/91


List of 100 categories

To fetch the first 100 categories from a WordPress web site :

https://admin.ki-leierbud.lu/wp-json/wp/v2/categories?per_page=100

[{"id":77,"count":0,"description":"","link":"https:\/\/admin.ki-leierbud.lu\/de\/archives\/category\/wissen\/ausbildung","name":"Ausbildung","slug":"ausbildung","taxonomy":"category","parent":29,"meta":[],"_links":{"self":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/77","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories"}],"about":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/taxonomies\/category"}],"up":[{"embeddable":true,"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/29"}],"wp:post_type":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/posts?categories=77"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}},

{"id":212,"count":2,"description":"","link":"https:\/\/admin.ki-leierbud.lu\/de\/archives\/category\/wissen\/fragen\/ausbildung-ki","name":"Ausbildung KI","slug":"ausbildung-ki","taxonomy":"category","parent":35,"meta":[],"_links":{"self":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/212","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories"}],"about":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/taxonomies\/category"}],"up":[{"embeddable":true,"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/35"}],"wp:post_type":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/posts?categories=212"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}},

{"id":33,"count":0,"description":"","link":"https:\/\/admin.ki-leierbud.lu\/pt\/archives\/category\/conhecimento","name":"Conhecimento","slug":"conhecimento","taxonomy":"category","parent":0,"meta":[],"_links":{"self":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/33","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories"}],"about":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/taxonomies\/category"}],"wp:post_type":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/posts?categories=33"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}},

{"id":27,"count":0,"description":"","link":"https:\/\/admin.ki-leierbud.lu\/fr\/archives\/category\/connaissances","name":"Connaissances","slug":"connaissances","taxonomy":"category","parent":0,"meta":[],"_links":{"self":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories\/27","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/categories"}],"about":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/taxonomies\/category"}],"wp:post_type":[{"href":"https:\/\/admin.ki-leierbud.lu\/wp-json\/wp\/v2\/posts?categories=27"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}},

Here is the list of the four first categories returned in the JSON file with their parent categories :

  • 77 >>> 29
  • 212 >>> 35
  • 33 >>> 0
  • 27 >>> 0

A “0” means that the category has no parent.

Here is a list with the returned categories to view the details provided :


CATEGORY from POST

To extract a category from a post JSON file :

...... {"footnotes":""},"categories":[93],"tags":[201],"class_list":["post-410","post","type-post","status-publish","format-standard","hentry","category-image-style-easy-en-en","tag-simpson"],"translations":{"en":410},"_links":.....

Parent, Grand-parent and top CATEGORIES

To find the parent, grand-parent or top level categories from a post JSON file , we must proceed iteratively:

  1. fetch the POST JSON file and extract first category ID
  2. fetch the first CATEGORY JSON file and extract the parent category ID
  3. if the parent category is not 0, continue to fetch the PARENT CATEGORY JSON file and extract the grand-parent category ID
  4. if the grand-parent category is not 0, continue to fetch the GRAND-PARENT CATEGORY JSON file and extract the next level category ID

Hosting

KI-Léierbud.lu Homepage

in root folder /httpdocs/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="theme-color" content="#fff7e6" />
  <title>KI-Léierbud</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      height: 100%;
      background-color: #fff7e6;
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      color: #333;
      line-height: 1.6;
      overflow-x: hidden;
      -webkit-overflow-scrolling: touch;
    }
    body {
      padding-top: env(safe-area-inset-top);
      padding-right: env(safe-area-inset-right);
      padding-bottom: env(safe-area-inset-bottom);
      padding-left: env(safe-area-inset-left);
    }
    main {
      max-width: 768px;
      margin: 0 auto;
      padding: 1em;
    }
    h1 {
      font-size: 2.8em;
      text-align: center;
      margin-top: 1em;
      color: #b30000;
    }
    .indented-lines p {
      margin: 0.3em 0;
      color: #444;
    }
    .indent-0 { padding-left: 0; }
    .indent-1 { padding-left: 1em; }
    .indent-2 { padding-left: 2em; }
    .indent-3 { padding-left: 3em; }
    .indent-4 { padding-left: 4em; }
    .timeline {
      text-align: center;
      font-weight: bold;
      font-size: 1.1em;
      margin: 1.5em 0 0.5em;
      color: #5a2d0c;
    }
    img {
      display: block;
      width: 100%;
      height: auto;
      margin: 1em 0;
      box-shadow: 0 4px 10px rgba(0,0,0,0.1);
      border-radius: 10px;
    }
    .language-links {
      margin-top: 2em;
    }
    .language-links p {
      margin: 0.3em 0;
    }
  </style>
</head>
<body>
  <main>
    <div class="indented-lines">
      <p class="indent-0"><a href="https://admin.ki-leierbud.lu">Welcome to the portal of</a> …</p>
      <p class="indent-1"><a href="https://admin.ki-leierbud.lu/">Bienvenue sur le portail de</a> …</p>
      <p class="indent-2"><a href="https://admin.ki-leierbud.lu">Willkommen auf dem Portal der</a> …</p>
      <p class="indent-3"><a href="https://admin.ki-leierbud.lu">Bem-vindos ao portail de</a> …</p>
      <p class="indent-4"><a href="https://admin.ki-leierbud.lu/">Wëllkomm um Portal vun der</a> …</p>
    </div>

    <h1>KI-Léierbud</h1>

    <div class="timeline">&lt;&lt;&lt; 14 .000 .000 .000 <a href="https://admin.ki-leierbud.lu/archives/99">years</a> - <a href="https://admin.ki-leierbud.lu/archives/108">années</a> – <a href="https://admin.ki-leierbud.lu/archives/102">Jahre</a> – <a href="https://admin.ki-leierbud.lu/archives/106">anos</a> - <a href="https://admin.ki-leierbud.lu/archives/104">Joer</a></div>
    <img src="images/14billions.jpg" alt="14 Billion Years" />

    <div class="timeline">&lt;&lt;&lt; 14.000.000 années – Jahre – years – Joer – anos</div>
    <img src="images/14millions.jpg" alt="14 Million Years" />

    <div class="timeline">&lt;&lt;&lt; 14.000 années – Jahre – years – Joer – anos</div>
    <img src="images/14thousand.jpg" alt="14 Thousand Years" />

    <div class="timeline">&lt;&lt;&lt; 14 années – Jahre – years – Joer – anos</div>
    <img src="images/14years.jpg" alt="14 Years" />

    <div class="timeline">2023 : AI Tsunami (en) – Tsunami IA (fr, pt) – KI Tsunami (de, lb)</div>
    <img src="images/tsunami.jpg" alt="AI Tsunami" />

    <div class="timeline">aujourd’hui – heute – today – haut – hoje &gt;&gt;&gt; KI-Léierbud</div>
    <img src="images/ki-leierbud.jpg" alt="KI-Léierbud Today" />

    <div class="language-links indented-lines">
      <p class="indent-0">Start your exploration of the KI-Léierbud in English </p>
      <p class="indent-1">Commencez votre exploration de la KI-Léierbud en français </p>
      <p class="indent-2">Beginnen Sie Ihre Erkundung der KI-Léierbud auf Deutsch </p>
      <p class="indent-3">Comece a sua exploração da KI-Léierbud em português </p>
      <p class="indent-4">Start Är Entdeckung vun der KI-Léierbud op Lëtzebuergesch </p>
    </div>
  </main>
</body>
</html>

File .htaccess

in root folder /ki-leierbud.lu/httpdocs/.htaccess

# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

Lux-ASR: Speech to Text System

The University of Luxembourg provides and Automatic Speech Recognition System (ASR) for Luxembourgish and several other languages (english, french, german, portuguese and spanish). Four output formats are available : plain text (txt), SubRip Subtitles (srt), JSON (with or without time codes for words) and Praat TextGrid. As an experimental feature for the Luxembourgish text translation to other languages has been added, which will output the recognized text in English, French, German, Portuguese, and Spanish.

The speech to transcribe can be recorded from a microphone or uploaded as Audio or Video file. If the recording contains more than one speaker, setting diarization to “On” will separate the text of every speaker in the recording along with time codes for their turns.

API Access

Lux-ASR can also be accessed by API and can be reached via:

curl -X POST "https://luxasr.uni.lu/v2/asr?diarization=Enabled&outfmt=text" \
  -H "accept: application/json" \
  -F "audio_file=@PATH/TO/AUDIO FILE;type=audio/wav"

The API returns the transcription in the specified output format.

Query Parameters

  • diarization: Can be set to Enabled (default) or Disabled to include or exclude speaker diarization.
  • outfmt: Specifies the output format. Supported values are:
    • text – plain text transcript (default)
    • json – detailed JSON output
    • srt – SubRip subtitle format
    • textgrid – Praat TextGrid format

Accepted audio formats are .wav, .mp3, and .m4a.

Python Script

Below is a basic Python script that replicates the functionality of the curl command with added flexibility. You can specify the audio file and optionally choose whether to enable diarization and which output format to use.

import requests
import argparse
import os
import sys

def main():
    parser = argparse.ArgumentParser(
        description="Send an audio file to the LuxASR API for transcription."
    )
    parser.add_argument(
        "audio_file",
        type=str,
        help="Path to the audio file (.wav, .mp3, .m4a)"
    )
    parser.add_argument(
        "--diarization",
        choices=["Enabled", "Disabled"],
        default="Enabled",
        help="Enable or disable speaker diarization (default: Enabled)"
    )
    parser.add_argument(
        "--outfmt",
        choices=["text", "json", "srt", "textgrid"],
        default="text",
        help="Output format: text, json, srt, or textgrid (default: text)"
    )

    args = parser.parse_args()

    if not os.path.isfile(args.audio_file):
        print(f"Error: File '{args.audio_file}' not found.")
        sys.exit(1)

    url = f"https://luxasr.uni.lu/v2/asr?diarization={args.diarization}&outfmt={args.outfmt}"
    headers = {
        "accept": "application/json"
    }

    # Determine MIME type
    ext = args.audio_file.lower()
    if ext.endswith(".wav"):
        mime_type = "audio/wav"
    elif ext.endswith(".mp3"):
        mime_type = "audio/mpeg"
    elif ext.endswith(".m4a"):
        mime_type = "audio/mp4"
    else:
        mime_type = "application/octet-stream"

    with open(args.audio_file, "rb") as audio:
        files = {
            "audio_file": (os.path.basename(args.audio_file), audio, mime_type)
        }
        response = requests.post(url, headers=headers, files=files)

    print(response.text)

if __name__ == "__main__":
    main()

Usage

python luxasr_transcribe.py path/to/your_audio.wav --diarization Enabled --outfmt json

Replace path/to/your_audio.wav with your actual audio file. The --diarization and --outfmt options are optional and default to Enabled and text respectively.

Lux-ASR is under constant development by Peter Gilles, Nina Hosseini-Kivanani, and Léopold Hillah at the University of Luxembourg and is supported by the Chambre des Députes du Grand-Duché de Luxembourg.

Disclaimer

Note that the transcription and the translation are run on a dedicated server at the University of Luxembourg. All data thus stays within Luxembourg and the University’s network. Nobody has access to the uploaded audio or the text output. The audio data is streamed to this server and no files are stored on this server or in the network. No data is used to further train the model and no data is transferred to third parties.