Source: backend/models/modelKeywords.js

/**
 * @fileoverview Diese Datei enthält Funktionen zur Initialisierung eines MPNet-Modells 
 * und zur Generierung von Schlüsselwörtern aus Text. Sie ermöglicht das Laden des Modells 
 * aus dem lokalen Speicher und die Berechnung von Ähnlichkeiten zwischen Text- und Wort-Embeddings.
 * 
 * @author Ayoub, Lennart
 * Die Funktionen wurden mit Unterstützung von KI-Tools angepasst und optimiert.
 * @module modelKeywords
 */

const path = require('path');
const { performance } = require('perf_hooks');

let model;
let pipeline;
let env;

/**
 * Initialisiert das MPNet-Modell zur Extraktion von Schlüsselwörtern, falls es nicht bereits geladen wurde.
 * 
 * @async
 * @function initModel
 * @returns {Promise<Object>} Das initialisierte Modell.
 * @throws {Error} Falls das Modell nicht gefunden oder nicht geladen werden kann.
 */
async function initModel() {
    if (!model) {
        console.log('Initializing MPNet model for keywords...');
        try {
            const transformers = await import('@xenova/transformers');
            pipeline = transformers.pipeline;
            env = transformers.env;

            const baseModelPath = path.join(process.cwd(), 'node_modules', '@xenova', 'transformers', 'models');
            const modelName = 'Xenova/all-mpnet-base-v2';

            env.localModelPath = baseModelPath;
            env.cacheDir = baseModelPath;
            env.allowRemoteModels = false;

            const modelPath = path.join(baseModelPath, 'Xenova', 'all-mpnet-base-v2');
            console.log('Looking for model in:', modelPath);

            try {
                model = await pipeline('feature-extraction', modelName, {
                    quantized: true,
                    local: true,
                    revision: 'main',
                    modelPath: modelPath,
                    progress_callback: (progress) => {
                        if (progress) {
                            console.log(`Loading progress: ${Math.round(progress * 100)}%`);
                        }
                    }
                });
                console.log('Model loaded successfully from local storage');
            } catch (localError) {
                console.error('Local loading error:', localError.message);
                throw new Error(`Model not found locally at ${modelPath}. Please ensure the model is downloaded.`);
            }
        } catch (error) {
            console.error('Error loading model:', error);
            throw error;
        }
    }
    return model;
}

/**
 * Generiert Schlüsselwörter aus einem gegebenen Text mithilfe eines MPNet-Embeddings.
 * 
 * @async
 * @function generateKeywords
 * @param {string} text - Der Eingabetext, aus dem Schlüsselwörter extrahiert werden.
 * @param {number} [maxKeywords=2] - Die maximale Anzahl an zurückgegebenen Schlüsselwörtern.
 * @returns {Promise<string[]>} Eine Liste der extrahierten Schlüsselwörter.
 * @throws {Error} Falls das Modell nicht geladen werden kann oder ein Fehler während der Berechnung auftritt.
 * @example
 * const keywords = await generateKeywords("Dies ist ein Beispieltext für KI-gestützte Analyse.", 3);
 * console.log(keywords); // ["Analyse", "Beispieltext", "KI"]
 */
async function generateKeywords(text, maxKeywords = 2) {
    const startTime = performance.now();
    console.log('Starting keyword generation...');

    try {
        await initModel();

        // Text in Wörter aufteilen und Duplikate/kurze Wörter entfernen
        const words = [...new Set(text.toLowerCase()
            .match(/\b\w+\b/g)
            ?.filter(word => word.length > 3) || [])];

        if (words.length === 0) {
            return [];
        }

        // mbedding für den vollstöndigen Text abrufen
        const textEmbedding = await model(text, {
            pooling: 'mean',
            normalize: true
        });

        // embedding für einzelne Wörter abrufen
        const wordEmbeddings = await Promise.all(
            words.map(async word => ({
                word,
                embedding: await model(word, {
                    pooling: 'mean',
                    normalize: true
                })
            }))
        );

        // Berechnt similarity scores
        const keywordScores = wordEmbeddings.map(({ word, embedding }) => ({
            word,
            score: calculateCosineSimilarity(textEmbedding.data, embedding.data)
        }));

        // sortiere die Keywords nach score und wähle die besten aus
        const keywords = keywordScores
            .sort((a, b) => b.score - a.score)
            .slice(0, maxKeywords);

        const endTime = performance.now();
        console.log(`Keyword generation completed in ${(endTime - startTime).toFixed(2)}ms`);
        console.log('Keywords found:', keywords);

        return keywords.map(k => k.word);

    } catch (error) {
        console.error('Error generating keywords:', error);
        throw error;
    }
}

/**
 * Berechnet die Kosinusähnlichkeit zwischen zwei Vektoren.
 * 
 * @function calculateCosineSimilarity
 * @param {number[]} vec1 - Der erste Vektor.
 * @param {number[]} vec2 - Der zweite Vektor.
 * @returns {number} Ein Wert zwischen -1 und 1, der die Ähnlichkeit der beiden Vektoren beschreibt.
 * @example
 * const similarity = calculateCosineSimilarity([0.1, 0.2, 0.3], [0.1, 0.25, 0.35]);
 * console.log(similarity);
 */
function calculateCosineSimilarity(vec1, vec2) {
    const dotProduct = vec1.reduce((sum, val, i) => sum + val * vec2[i], 0);
    const norm1 = Math.sqrt(vec1.reduce((sum, val) => sum + val * val, 0));
    const norm2 = Math.sqrt(vec2.reduce((sum, val) => sum + val * val, 0));
    return dotProduct / (norm1 * norm2);
}

module.exports = { generateKeywords };