CRAN 0.1.8 | GitHub 0.1.9

Project Status: Active – The project has reached a stable, usable state and is being actively developed. R-CMD-check Downloads Total DOI Sponsor

transforEmotion: Transformer Emotion Analysis for Text, Image, Video, and Speech

Logo

With transforEmotion you can use transformer models for zero-shot emotion classification of text, images, video, and speech in R, and find faces and code them by hand — without the need for a GPU, subscriptions, or paid services, and without any manual Python setup. All data is processed locally on your machine, and nothing is sent to any external server or third-party service. This ensures full privacy for your data.

Installation and First Run

Install the release from CRAN, or the development version from GitHub:

install.packages("transforEmotion")

# Development version
install.packages("remotes")
remotes::install_github("atomashevic/transforEmotion")

You do not need to install Python. The first function that uses Python sets it up:

  1. reticulate downloads uv, a fast Python package manager, if it is not installed.
  2. uv installs Python 3.12 and the Python packages: about 500 MB, or about 3 GB on a machine with an NVIDIA GPU (the CUDA build of PyTorch).
  3. The function downloads its model from Hugging Face, usually 100–600 MB.

This takes 1–3 minutes and happens once. Later R sessions reuse the cached environment and models: Python starts in a few seconds and nothing is downloaded again. Each new model you use is downloaded the first time you use it.

To do the setup before your first analysis, and download the default text, image and sentence-similarity models (about 1 GB):

library(transforEmotion)
setup_modules()

Optional features add their Python packages the first time they are used: rag() adds LlamaIndex and downloads a language model (TinyLLAMA, about 2 GB), video_scores() adds pytubefix for YouTube URLs, speech_scores() adds av (PyAV) to read audio, and the FindingEmo functions add findingemo-light. To prepare them in advance, run setup_modules(extras = c("rag", "youtube", "audio", "findingemo")).

Then run an analysis:

library(transforEmotion)
transformer_scores(
  text = "I am so happy to see you today!",
  classes = c("joy", "anger", "fear")
)
#> $`I am so happy to see you today!`
#>         joy       anger        fear
#> 0.992855728 0.003367609 0.003776714

The scores are the model’s probabilities that the text belongs to each class.

The Python environment and the models are stored in cache folders in your home directory (uv’s cache and ~/.cache/huggingface). To keep them elsewhere, for example on a cluster with a small home quota, or to work without internet access, see Offline Use.

If the setup fails, the error shows uv’s output. Fix the cause (usually the network connection) and try again in a new R session: Python cannot be set up again once it has started in a session.

[!WARNING] If you use the radian console (VSCode/terminal), its Python session may block first-time environment provisioning. Use the default R console for initial setup, then switch back if you prefer.

[!NOTE] If you are using RStudio, please make sure that you are using the latest version of both R and RStudio.

Google Colab

Open In Colab

The notebook installs the package in a Colab R runtime and runs the text, image, video and RAG examples. It works on the free CPU runtime; on a GPU runtime (Runtime > Change runtime type > T4 GPU) the package installs the CUDA build of PyTorch and uses the GPU automatically. Colab deletes the Python environment and models when the runtime is deleted, so a new runtime repeats the first-run setup.

Text Example

The example below uses item descriptions from the personality trait extraversion in the NEO-PI-R inventory found on the IPIP website.

# Load data
data(neo_ipip_extraversion)

For the example, the positively worded item descriptions will be used.

# Example text
text <- neo_ipip_extraversion$friendliness[1:5]

Next, the text can be loaded in the function transformer_scores() to obtain the probability that item descriptions correspond to a certain class. The classes defined below are the facets of extraversion in the NEO-PI-R. The example text data draws from the friendliness facet.

# Cross-Encoder DistilRoBERTa
transformer_scores(
 text = text,
 classes = c(
   "friendly", "gregarious", "assertive",
   "active", "excitement", "cheerful"
 )
)

The default transformer model is DistilRoBERTa. The model is fast and accurate.

BART

Another model that can be used is BART, a much larger and more computationally intensive model (slower prediction times). The BART model tends to be more accurate but the accuracy gains above DistilRoBERTa are negotiatiable.

# Facebook BART Large
transformer_scores(
 text = text,
 classes = c(
   "friendly", "gregarious", "assertive",
   "active", "excitement", "cheerful"
 ),
 transformer = "facebook-bart"
)

Any Text Classification Model with a Pipeline on huggingface

Text classification models with a pipeline on huggingface can be used so long as there is a pipeline available for them. Below is an example of Typeform’s DistilBERT model.

# Directly from huggingface: typeform/distilbert-base-uncased-mnli
transformer_scores(
 text = text,
 classes = c(
   "friendly", "gregarious", "assertive",
   "active", "excitement", "cheerful"
 ),
 transformer = "typeform/distilbert-base-uncased-mnli"
)

RAG

The rag function is designed to enhance text generation using Retrieval-Augmented Generation (RAG) techniques. This function allows users to input text data or specify a path to local PDF files, which are then used to retrieve relevant documents.

Supported local LLMs include TinyLLAMA, Gemma3‑1B/4B, Qwen2.5‑1.5B/3B, Qwen3‑1.7B, and Ministral‑3B via HuggingFace — no Ollama required. Specifically: google/gemma-3-1b-it, google/gemma-3-4b-it, Qwen/Qwen2.5-1.5B-Instruct, Qwen/Qwen2.5-3B-Instruct, Qwen/Qwen3-1.7B-Instruct, and ministral/Ministral-3b-instruct. The default model is TinyLLAMA for speed. Gemma 3 is gated (a Hugging Face token is needed) and requires transformers >= 4.50; Qwen 2.5 is not gated and works with the transformers version the package installs.

Here’s an example based on the decription of this package. First, we specify the text data.

text <- "With `transforEmotion` you can use cutting-edge transformer models for zero-shot emotion
        classification of text, image, and video in R, *all without the need for a GPU,
        subscriptions, paid services, or using Python. Implements sentiment analysis
        using [huggingface](https://huggingface.co/) transformer zero-shot classification model pipelines.
        The default pipeline for text is
        [Cross-Encoder's DistilRoBERTa](https://huggingface.co/cross-encoder/nli-distilroberta-base)
        trained on the [Stanford Natural Language Inference](https://huggingface.co/datasets/stanfordnlp/snli) (SNLI) and
        [Multi-Genre Natural Language Inference](https://huggingface.co/datasets/nyu-mll/multi_nli) (MultiNLI) datasets.
        Using similar models, zero-shot classification transformers have demonstrated
        superior performance relative to other natural language processing models
        (Yin, Hay, & Roth, [2019](https://arxiv.org/abs/1909.00161)).
        All other zero-shot classification model pipelines can be implemented using their model name
        from https://huggingface.co/models?pipeline_tag=zero-shot-classification."

And then we run the rag function.

 rag(text, query = "What is the use case for transforEmotion package?")

This code will provide the output similar to this one.

The use case for transforEmotion package is to use cutting-edge transformer
models forzero-shot emotion classification of text, image, and video in R,
without the need for a GPU, subscriptions, paid services, or using Python.
This package implements sentiment analysis using the Cross-Encoder's DistilRoBERTa
model trained on the Stanford Natural Language Inference (SNLI) and MultiNLI datasets.
Using similar models, zero-shot classification transformers have demonstrated
superior performance relative to other natural language processing models
(Yin, Hay, & Roth, [2019](https://arxiv.org/abs/1909.00161)).
The transforEmotion package can be used to implement these models and other
zero-shot classification model pipelines from the HuggingFace library.>

Supported local LLMs include TinyLLAMA, Gemma3‑1B/4B, Qwen2.5‑1.5B/3B, Qwen3‑1.7B, and Ministral‑3B via HuggingFace — no Ollama required. Specifically: google/gemma-3-1b-it, google/gemma-3-4b-it, Qwen/Qwen2.5-1.5B-Instruct, Qwen/Qwen2.5-3B-Instruct, Qwen/Qwen3-1.7B-Instruct, and ministral/Ministral-3b-instruct.

[!IMPORTANT] Gemma 3 access - Gemma 3 repos are gated. You must log in to Hugging Face and accept the model license on the model page (e.g., https://huggingface.co/google/gemma-3-1b-it).

[!TIP] Hugging Face token handling - No long-term storage: The package never writes tokens to disk. - Gemma only: It first tries to download without a token; on 401/forbidden, it prompts you to paste a token and uses it once for that download, then discards it. - Scripts and notebooks (including Google Colab) cannot show the prompt: set the token first with Sys.setenv(HF_TOKEN = "hf_..."). - Public models: Downloads always ignore tokens to avoid unnecessary 401s from stale credentials. - Create a token at https://huggingface.co/settings/tokens. For Gemma 3, accept the model license on the model page first.

You can also request structured outputs for easier parsing and statistics.

# JSON output (validated schema)
j <- rag(text, query = "Extract emotions present in the text", output = "json", task = "emotion")

# Tidy table output
rag(text, query = "Extract emotions present in the text", output = "table", task = "emotion")

# Helpers for parsing/flattening
as_rag_table(j)
parse_rag_json(j)

RAG structured outputs (per-document)

For per-document emotion/sentiment with small local LLMs (Gemma3‑1B/4B or Qwen2.5‑1.5B/3B), use the convenience wrapper:

texts <- c(
  "I feel so happy and grateful today!",
  "This is frustrating and makes me angry."
)
rag_sentemo(texts, task = "emotion", output = "table", transformer = "Qwen2.5-3B")

VAD Example

Directly predict Valence–Arousal–Dominance (VAD) with definitional labels and automatic fallbacks:

texts <- c("I'm absolutely thrilled!", "I feel so helpless and sad", "This is boring")
vad_scores(texts, input_type = "text")

# Single dimension, simple labels
vad_scores(texts, input_type = "text", dimensions = "valence", label_type = "simple")

Image Example

For Facial Expression Recognition (FER) task from images we use Open AI’s CLIP transformer model. Two input arguments are needed: the path to image and list of emotion labels.

Path can be either local or an URL. Here’s an example of using a URL of Mona Lisa’s image from Wikipedia.


# Image URL or local filepath
image <- 'https://cdn.mos.cms.futurecdn.net/xRqbwS4odpkSQscn3jHECh-650-80.jpg'

# Array of emotion labels
emotions <- c("excitement", "happiness", "pride", "anger", "fear", "sadness", "neutral")

# Run FER with base model
image_scores(image, emotions, model = "oai-base")

You can define up to 10 emotions. The output is a data frame with 1 row and columns corresponding to emotions. The values are FER scores for each emotion.

If there is no face detected in the image, the output is one row of NA scores.

If there are multiple faces detected in the image, by default the function will return the FER scores for the largest (focal) face. Alternatively, you can select the face on the left or the right side of the image by specifying the face_selection argument, or score every face with face_selection = "all".

Faces are found with OpenCV’s YuNet detector. To reproduce scores from versions before 0.1.8, which used a Haar cascade, set face_detector = "haar".

Inspecting and Choosing Faces

detect_faces() lists every face it finds in images or in frames sampled from a video, with its box, confidence, a track id that follows it from frame to frame, and a person id shared by all faces of the same person (OpenCV’s SFace face recognizer; its 10 MB model is downloaded on first use). Pick the faces to analyse with select_faces() or ordinary subsetting, and score them with face_scores():

faces <- detect_faces("interview.mp4", nframes = 50)
table(faces$person_id)

# Draw the boxes, track and person on a few frames
draw_faces(faces, max_images = 6)

# Every face of one person, across the whole video
scores <- face_scores(select_faces(faces, persons = "p1"), classes = emotions)

To look at the faces in your web browser instead, use view_faces(faces): R opens a page that shows every image or sampled frame with its boxes, coloured by person, and a list of the people found. With select = TRUE, you click faces, video tracks or people and then Send to R, and view_faces() returns those faces:

chosen <- view_faces(faces, select = TRUE)
scores <- face_scores(chosen, classes = emotions)

The page is served by R on your own computer only and makes no outside requests. When R runs on a remote computer, forward the port over SSH (view_faces() prints the command), or use live = FALSE to write the page as a folder to copy, and read its exported choice with read_face_selection().

In a Jupyter notebook, such as Google Colab, view_faces() shows the page below the cell instead. Click the faces, then Copy R code, and run the copied line in a new cell:

view_faces(faces, select = TRUE)
# then, in a new cell, the copied line, for example
chosen <- read_face_selection("te-selection:eyJzY2hlbWEiOi...", faces)

To code faces by hand, for example to check the model against people, annotation_task(faces, classes = emotions, coders = c("C01", "C02"), dir = "round-1") sets up a coding task, annotate_faces() opens the coding page and saves each answer as it is given, and read_annotations(), annotation_agreement() and consensus_labels() read the answers, measure agreement between coders and give one label per face for evaluate_emotions(). In a Jupyter notebook such as Google Colab, annotate_faces() shows the coding page below the cell, and Copy R code gives a line, answers <- read_annotations(task, "te-annotations:..."), to run in a new cell. The vignette “Checking faces and coding them by hand” walks through it.

The same tasks can hold video clips, images or texts instead of single faces, for example to review the clips a model got wrong. Each item shows sampled frames with the face boxes, the clip with its sound and its text; the reference label, the model’s answer and its scores appear after the coder’s own answer, followed by a question on what explains the result:

emotions <- c("angry", "fearful", "happy", "sad")
items <- data.frame(id = errors$clip, media = errors$path, text = errors$description,
                    label = errors$label, prediction = errors$predicted, errors[, emotions])
review <- annotation_task(items = items, classes = emotions, coders = c("C01", "C02"), dir = "error-review")
annotate_faces(review)

answers <- read_annotations(review)
table(answers$value[answers$question == "why"])
consensus <- consensus_labels(answers)
evaluate_emotions(consensus, truth_col = "reference", pred_col = "label")  # coders vs the dataset's label

To try this, the package comes with a picture of two people (system.file("extdata", "two-people.jpg", package = "transforEmotion")) and a short clip in which both leave and one of them comes back alone ("two-people.mp4").

The same table can be passed as face_selection to image_scores(), image_scores_dir() and video_scores(). Grouping faces by person processes biometric data; use identify = FALSE to turn it off.

Video Example

Video processing works by extracting frames from the video and then running the image processing function on each frame. Two input arguments are needed: the path to video and list of emotion labels.

Path can be either a local filepath or a YouTube URL. Support for other video hosting platforms is not yet implemented.

# Video URL or local filepath
video_url <- "https://www.youtube.com/watch?v=hdYNcv-chgY&ab_channel=Conservatives"

# Array of emotion labels
emotions <- c("excitement", "happiness", "pride", "anger", "fear", "sadness", "neutral")

# Run FER on `nframes` of the video with large model
result <- video_scores(video_url, classes = emotions,
                    nframes = 10, save_video = TRUE,
                    save_frames = TRUE, video_name = 'boris-johnson',
                    start = 10, end = 120, model = "oai-large")

Working with videos is more computationally complex. This example extracts only 10 frames from the video and shouldn’t take longer than a few minutes on an average laptop without GPU (depending on your internet connection needed to download the entire video and CLIP model). In research applications, we will usually extract 100-300 frames from the video. This can take much longer, so patience is advised while waiting for the results.

Available Models

The image_scores and video_scores functions support different models. The available models are:

Fine-tuned vision encoders

oai-base-fer and oai-large-fer are checkpoints that hold only a fine-tuned CLIP vision encoder. transforEmotion loads the text encoder and processor from the base CLIP model named in the checkpoint’s config.json and replaces its vision encoder, so the labels still work zero-shot. Any Hugging Face checkpoint of this kind (model type clip_vision_model, saved with save_pretrained() from a CLIP model) can be used the same way, by its ID or through register_vision_model(architecture = "clip").

The fine-tuned models recognise facial expressions better than the originals, and label wording matters too. On a sample of the MAFW video dataset (100 clips per basic emotion, of which 682 had a detectable face; 10 frames per clip, face_selection = "largest", clip prediction = highest mean score), accuracy for seven basic emotions was:

Model "angry", "happy", … "a photo of an angry face", …
oai-base 0.27 0.33
oai-large 0.33 0.34
oai-base-fer 0.41 0.40
oai-large-fer 0.44 0.48

Chance is 0.14. Validate the model and label wording on a labelled sample from your own data before a full analysis (see evaluate_emotions()).

emotions <- c("a photo of an angry face", "a photo of a disgusted face", "a photo of a fearful face",
              "a photo of a happy face", "a photo of a neutral face", "a photo of a sad face",
              "a photo of a surprised face")
image <- system.file("extdata", "boris-1.png", package = "transforEmotion")
image_scores(image, classes = emotions, model = "oai-large-fer")

Note: The memory requirements listed above are approximate and represent the minimum RAM needed. For optimal performance, we recommend having at least 16GB of system RAM when using any of these models. If you’re processing videos or multiple images in batch, more RAM might be needed. When using GPU acceleration, similar VRAM requirements apply. We recommend using ‘oai-base’ or ‘oai-large’ for most applications as they provide a good balance between accuracy and resource usage.

Speech Example

speech_scores() splits the audio track of an audio file, a video file or a YouTube video into segments and scores each segment against the classes with a CLAP audio-text model (laion/larger_clap_music_and_speech), zero-shot, as image_scores() does with CLIP. The segments are chosen as video_scores() chooses frames: nsegments, segment_length (at most 10 seconds), start, end and uniform.

emotions <- c("a person speaking angrily", "a person speaking happily",
              "a person speaking sadly", "a person speaking in a neutral voice")

# Consecutive 5-second segments from 10 s to 120 s
result <- speech_scores("interview.mp4", classes = emotions,
                        nsegments = 20, segment_length = 5, start = 10, end = 120)

The result has one row per segment, with segment, start and end (seconds in the file) and one column of scores per class.

LAION CLAP is a general audio model and separates speech emotions poorly. On RAVDESS (1440 acted English clips from 24 actors, one segment per clip, clip prediction = highest score), unweighted average recall was:

Classes "angry", "happy", … "a person speaking angrily", …
8 emotions (chance 0.125) 0.13 0.17
angry, happy, neutral, sad (chance 0.25) 0.28 0.33

Most clips were predicted as neutral. Use the scores for exploration only, and validate them on a labelled sample from your own data (see evaluate_emotions()).

Vision Model Registry (experimental)

Register custom/experimental vision models and list them for use in image_scores() and video_scores():

# Quick add of popular experimental models
setup_popular_models(c("blip-base", "align-base"))

# Show models
show_vision_models()

# Register a custom CLIP model
register_vision_model(
  name = "my-clip",
  model_id = "openai/clip-vit-base-patch32",
  architecture = "clip",
  description = "My CLIP baseline"
)

GPU Support

When an NVIDIA GPU is detected on Linux or Windows, the CUDA 12.6 build of PyTorch is installed; otherwise the CPU-only build is used (on macOS, PyTorch supports Apple Silicon GPUs directly). GPU use needs only an NVIDIA driver recent enough for CUDA 12 (R525 or newer); the CUDA libraries come with PyTorch, so no CUDA Toolkit or compiler is required.

To override the detection, set an environment variable in a new R session before loading the package (or add it to .Renviron):

Sys.setenv(TE_FORCE_CPU = "1")             # always use the CPU build
Sys.setenv(TRANSFOREMOTION_USE_GPU = "1")  # always use the CUDA build
library(transforEmotion)

Everything works in CPU mode, only slower.

Offline Use: HPC Clusters and Containers

HPC clusters

Compute nodes often have no internet access, and home folders often have small quotas. Keep the Python environment, Python itself and the models in one folder on shared project storage, prepare it once on a login node, and use it offline in jobs:

# Once, on a login node (with internet access)
library(transforEmotion)
setup_cache("/project/mylab/transforEmotion-cache", gpu = TRUE)
setup_modules(extras = "rag", models = "facebook/bart-large-mnli")

# In each job script (no internet access needed)
library(transforEmotion)
setup_cache("/project/mylab/transforEmotion-cache", offline = TRUE, gpu = TRUE)
scores <- transformer_scores(text, classes)

Login nodes usually have no GPU, so set gpu explicitly, to the same value in both places: TRUE if the compute nodes have NVIDIA GPUs, FALSE otherwise. Offline jobs can use the default models plus any passed to setup_modules(models = ). The cache folder must stay writable.

Docker and Apptainer

docker/Dockerfile builds an image with the package, a fixed Python environment and the default models, which runs offline by default:

docker build -f docker/Dockerfile -t transforemotion .                        # CPU
docker build -f docker/Dockerfile --build-arg EXTRAS=rag -t transforemotion:rag .
docker build -f docker/Dockerfile --build-arg GPU=1 -t transforemotion:cuda .  # NVIDIA
docker run --rm -it -v "$PWD":/work transforemotion

On clusters that run Apptainer (Singularity) instead of Docker, convert the image with apptainer build transforemotion.sif docker-daemon://transforemotion:latest. Apptainer images are read-only, which is why the image uses a fixed environment rather than uv’s cache.

To build your own fixed environment, for example in a read-only software folder, install the output of python_requirements() into a Python 3.12 virtual environment and set TRANSFOREMOTION_PYTHON to its Python executable; transforEmotion then uses it without installing anything.

Datasets: FindingEmo-Light

Reproducibly download and prepare the FindingEmo-Light dataset:

# Download (optionally limit images for quick start)
download_findingemo_data("data/findingemo", max_images = 200, randomize = TRUE)

# Load and inspect annotations
ann <- load_findingemo_annotations("data/findingemo")
head(ann)

Example Images

The example images included in this package (inst/extdata/) have different licensing terms:

Citation

If you use transforEmotion in your research, please cite:

Tomašević, A., Golino, H., & Christensen, A. P. (2026). transforEmotion: An open-source R package for emotion analysis using transformer-based generative AI models. Computational Communication Research, 8(2). https://doi.org/10.5117/CCR2026.2.2.TOMA

@article{tomasevic2026transforemotion,
  title   = {{transforEmotion}: An Open-Source {R} Package for Emotion Analysis Using Transformer-Based Generative {AI} Models},
  author  = {Toma{\v{s}}evi{\'c}, Aleksandar and Golino, Hudson and Christensen, Alexander P.},
  journal = {Computational Communication Research},
  year    = {2026},
  volume  = {8},
  number  = {2},
  doi     = {10.5117/CCR2026.2.2.TOMA},
  url     = {https://doi.org/10.5117/CCR2026.2.2.TOMA}
}

The same entry is available from R with citation("transforEmotion").

References

BART

Lewis, M., Liu, Y., Goyal, N., Ghazvininejad, M., Mohamed, A., Levy, O., … & Zettlemoyer, L. (2019). Bart: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. arXiv preprint arXiv:1910.13461.

RoBERTa

Liu, Y., Ott, M., Goyal, N., Du, J., Joshi, M., Chen, D., … & Stoyanov, V. (2019). Roberta: A robustly optimized bert pretraining approach. arXiv preprint arXiv:1907.11692.

CLIP

Radford, A., Kim, J. W., Hallacy, C., Ramesh, A., Goh, G., Agarwal, S., Sastry, G., Askell, A., Mishkin, P., Clark, J., Krueger, G., & Sutskever, I. (2021). Learning Transferable Visual Models From Natural Language Supervision. arXiv preprint arXiv:2103.00020

Comparison of Methods

Yin, W., Hay, J., & Roth, D. (2019). Benchmarking zero-shot text classification: Datasets, evaluation and entailment approach. arXiv preprint arXiv:1909.00161.