Source: sentimentAnalysis.js

const vader = require("vader-sentiment");
const Sentiment = require("sentiment");
const natural = require("natural");
const afinnSentiment = new natural.SentimentAnalyzer("English", natural.Afinn);
const winkSentiment = require("wink-sentiment");

import { env, pipeline } from "@xenova/transformers";

var sentimentAnalysis = require("sentiment-analysis");

const { defaultModel, getModelPath } = require("./models.js");

// Define the cache directory
env.cacheDir = path.join(__dirname, "../../.model_cache");

let classifier;

/**
 * Calculates a sentiment score for the provided text using the selected sentiment analysis engine.
 * Supports multiple engines: VADER, Hugging Face, Sentiment (AFINN-165), wink-sentiment,
 * sentiment-analysis (AFINN-111), Natural Node, and ml-sentiment.
 *
 * @param {string} text - The input text to analyze.
 * @param {string} [engine="vader"] - The engine to use. Options include:
 *   * 'vader', 'hugging-face', 'sentiment'(AFINN-165), 'wink-sentiment',
 *   * 'sentiment-analysis' (AFINN-111), 'natural'
 * @param {string} [model=""] -  Name of the Hugging Face model (if 'hugging-face' engine selected).
 * @returns {number} A sentiment score between -1 (most negative) and 1 (most positive).
 */

async function getSentiment(text, engine = "vader", model = "") {
  let sentimentScore = 0;

  switch (engine) {
    case "hugging-face":
      let hfModel = getModelPath(model);
      if (!hfModel) {
        hfModel = getModelPath(defaultModel);
      }
      sentimentScore = getHuggingFace(text, model);
      break;

    case "sentiment":
    case "AFINN-165":
      sentimentScore = getSentimentNPM(text);
      break;

    case "wink-sentiment":
      return getWinkSentiment(text);
      break;

    case "sentiment-analysis":
    case "AFINN-111":
      sentimentScore = getSentimentAnalysis(text);
      break;

    case "natural":
      sentimentScore = getNatural(text);
      break;

    case "vader":
    default:
      sentimentScore = getVader(text);
      break;
  }

  return sentimentScore;
}

/**
 * Calculates a sentiment score for the provided text using a Hugging Face sentiment analysis model.
 * Loads the model dynamically and normalizes the output score.
 *
 * @param {string} text - The input text to analyze.
 * @param {string} model - The name of the Hugging Face model.
 * @returns {number}  A sentiment score between -1 (most negative) and 1 (most positive).
 * @throws {Error} If there are issues loading or using the Hugging Face model.
 */

async function getHuggingFace(text, model) {
  try {
    if (!classifier) {
      classifier = await pipeline("sentiment-analysis", { model });
    }
  } catch (error) {
    console.error("Failed to load model:", error);
  }

  try {
    const result = await classifier(text);

    // Normalizing the score - assuming the result includes a label and score
    let normalizedScore = 0;

    // Example normalization logic
    if (result.label === "POSITIVE") {
      normalizedScore = result.score; // Map directly if positive
    } else if (result.label === "NEGATIVE") {
      normalizedScore = -result.score; // Negate if negative
    }

    // Ensure the score is within [-1, 1]
    normalizedScore = Math.max(-1, Math.min(1, normalizedScore));

    return normalizedScore;
  } catch (error) {
    console.error("Failed to classify text:", error);
  }
}

/**
 * Calculates a sentiment score for the provided text using the VADER sentiment analysis library.
 *
 * @param {string} text -  The input text to analyze.
 * @returns {number}  A sentiment score between -1 (most negative) and 1 (most positive).
 */

function getVader(text) {
  return vader.SentimentIntensityAnalyzer.polarity_scores(text).compound;
}

/**
 * Calculates a sentiment score using the 'sentiment' npm module (AFINN-165 based).
 *
 * @param {string} text - The input text to analyze.
 * @returns {number}  A sentiment score between -1 (most negative) and 1 (most positive).
 */

function getSentimentNPM(text) {
  const sentiment = new Sentiment();
  const result = sentiment.analyze(text);
  // Normalizing the score to be between -1 and 1
  let normalizedScore =
    result.score / Math.max(1, Math.abs(result.comparative));
  normalizedScore = Math.max(-1, Math.min(1, normalizedScore));
  return normalizedScore;
}

/**
 * Calculates a sentiment score using the 'sentiment-analysis' npm module (AFINN-111 based).
 *
 * @param {string} text - The input text to analyze.
 * @returns {number} A sentiment score in a range specific to the 'sentiment-analysis' module.
 *                   Output may need normalization to the [-1, 1] range.
 */

function getSentimentAnalysis(text) {
  return sentimentAnalysis(text);
}

/**
 * Calculates a sentiment score using the 'wink-sentiment' npm module.
 * Normalizes score from the module's output range to the standard [-1, 1] range.
 *
 * @param {string} text - The input text to analyze.
 * @returns {number} A sentiment score between -1 (most negative) and 1 (most positive).
 */

function getWinkSentiment(text) {
  const result = winkSentiment(text);
  // Normalize the score: result.score ranges from very negative to very positive
  const normalizedScore = Math.max(-1, Math.min(1, result.score / 5)); // Adjust 5 based on observed maximum scores
  return normalizedScore;
}

/**
 * Calculates a sentiment score using the Natural Node.js library with an AFINN-based analyzer.
 * Normalizes the score to the standard [-1, 1] range.
 *
 * @param {string} text - The input text to analyze.
 * @returns {number} A sentiment score between -1 (most negative) and 1 (most positive).
 */

function getNatural(text) {
  const tokenizedText = new natural.WordTokenizer().tokenize(text); // Tokenize input text
  const analysisResult = afinnSentiment.getSentiment(tokenizedText);

  // Normalizing the sentiment score to be between -1 and 1
  // Assuming that scores typically range between -5 to 5, adjust based on observed scores if needed
  const normalizedScore = Math.max(-1, Math.min(1, analysisResult / 5));
  return normalizedScore;
}

module.exports = { getSentiment };