Fabula is a Python package for analyzing how sentiment or emotions evolve across a document. It slices text into segments, scores each segment with a Transformers model, and optionally produces a smoothed narrative arc, according to custom smoothing parameters.
Language support: The bundled defaults target contemporary French models, but you can supply any Hugging Face sequence-classification model that matches your analysis labels.
- Per-segment scoring for sentiment or emotion analysis.
- Multiple segmentation strategies (sentence, paragraph, token windows, in-context chunks).
- Long-input handling with chunk pooling and in-context interpolation.
- Narrative arc generation with resampling + smoothing.
- CLI and Python API for batch runs and scripting.
Base install:
pip install fabulaBase install includes Transformers + Torch for model inference, plus SentencePiece and matplotlib.
from fabula.core import Fabula
from fabula.scorer import TransformersScorer
from fabula.plot import plot_arc_series
scorer = TransformersScorer(model="cmarkea/distilcamembert-base-sentiment")
fb = Fabula(scorer=scorer)
# Per-segment scoring
scores = fb.score("Bonjour. C'est une belle journée.")
print(scores[["rel_pos", "label", "score"]])
# Narrative arc
arc = fb.arc("Bonjour. C'est une belle journée.", n_points=50)
print(arc.x[:5], arc.y[:5])
# Multi-dimensional emotion arc (per-label probabilities)
fb_emotion = Fabula(scorer=TransformersScorer(model="astrosbd/french_emotion_camembert"), analysis="emotion")
emotion_arc = fb_emotion.arc("Bonjour. Quelle surprise.", score_col="probs")
plot_arc_series(emotion_arc, title="Emotions across the story", legend_title="Emotions")Score a document and return per-segment results:
fabula score my.txt --format jsonCompute a narrative arc:
fabula arc my.txt --n-points 100 --smooth-window 9Compute a multi-dimensional emotion arc (one column per label):
fabula arc my.txt --analysis emotion --score-col probs --format csvTwo French novel examples are included in the repository:
src/example/tartarin.txt(Alphonse Daudet, Tartarin sur les Alpes)src/example/lassomoir.txt(Émile Zola, L'Assommoir)
You can run a quick, reproducible demo with --dummy (no model downloads):
# Tartarin: segment-level sentiment scores
fabula score src/example/tartarin.txt --dummy --format csv > tartarin_scores.csv
# Tartarin: smoothed sentiment arc
fabula arc src/example/tartarin.txt --dummy --n-points 120 --smooth-window 9 --format csv > tartarin_arc.csv
# L'Assommoir: segment-level sentiment scores
fabula score src/example/lassomoir.txt --dummy --format csv > lassomoir_scores.csv
# L'Assommoir: smoothed sentiment arc
fabula arc src/example/lassomoir.txt --dummy --n-points 120 --smooth-window 9 --format csv > lassomoir_arc.csvIf you want model-based results instead of the dummy scorer, remove --dummy:
fabula arc src/example/tartarin.txt --n-points 120 --smooth-window 9 --plot tartarin_arc.png
fabula arc src/example/lassomoir.txt --n-points 120 --smooth-window 9 --plot lassomoir_arc.pngSource attribution for these bundled examples is documented in
src/example/README.md.
Fabula supports two analysis modes:
- sentiment (default)
- emotion
Default models (used when --model is not provided):
- Sentiment:
cmarkea/distilcamembert-base-sentiment - Emotion:
astrosbd/french_emotion_camembert
You can override them with any Hugging Face model ID:
fabula score my.txt --analysis emotion --model j-hartmann/emotion-english-distilroberta-baseChoose a segmentation strategy with --segment (CLI) or provide a custom segmenter in the Python API. Each segment yields a relative position (rel_pos) within the document.
Splits on sentence-ending punctuation (regex-based).
fabula score my.txt --segment sentenceSplits on blank lines (\n\n). Useful for prose or articles with clear paragraph breaks.
fabula score my.txt --segment paragraphUses the tokenizer to create overlapping windows. Requires a Transformers tokenizer (not available in --dummy mode).
fabula score my.txt --segment window --window-tokens 256 --stride-tokens 64 --min-tokens 16Scores sentences and coarse chunks, then blends their probabilities to preserve long-range context. Requires a tokenizer with offset mappings.
fabula score my.txt \
--segment in-context \
--chunk-tokens 1024 \
--chunk-stride-tokens 1024 \
--chunk-min-tokens 128 \
--chunk-weight 0.3 \
--chunk-attention-tau 0.1chunk-weightcontrols how much chunk scores influence sentence scores.chunk-attention-taucontrols the distance decay for chunk influence.
For very long segments, you can pool probabilities across overflowing chunks instead of truncating. Pooling options:
none(default)meanmaxattention(softmax-weighted by max prob per chunk)
fabula score my.txt --pooling mean --pooling-stride-tokens 128The arc command (or Fabula.arc) turns segment scores into a continuous curve.
- Resampling: points are interpolated to
--n-points. - Smoothing: set
--smooth-methodtomoving_average,gaussian, ornone. - Padding mode:
reflect,edge, orconstant.
Example:
fabula arc my.txt \
--n-points 200 \
--smooth-method gaussian \
--smooth-window 11 \
--smooth-sigma 2.0 \
--smooth-pad-mode reflectIf your segments include multiple labels (e.g., emotions), Fabula can emit one arc per label instead of collapsing everything into a single scalar.
Two ways to request vector arcs:
- Per-label probabilities from the
probscolumn:
fabula arc my.txt --analysis emotion --score-col probs --format csvThis produces a CSV like:
x,JOIE,TRISTESSE,PEUR,SURPRISE
0.0,0.12,0.55,0.07,0.26
0.01,0.15,0.52,0.06,0.27
...
- Explicit columns using
--score-cols(comma-separated). This is useful if you already have multiple numeric columns in your scoring output and want independent arcs for each:
fabula arc my.txt --score-cols valence,arousal,dominanceThe scalar plot function fills under a single curve. For multi-dimensional arcs,
use plot_arc_series to get multiple lines and a legend:
from fabula import Fabula, plot_arc_series
from fabula.scorer import TransformersScorer
fb = Fabula(scorer=TransformersScorer(model="astrosbd/french_emotion_camembert"), analysis="emotion")
arc = fb.arc("Bonjour. Quelle surprise.", score_col="probs")
plot_arc_series(arc, title="Smoothed Evolution of Emotions", legend_title="Emotions")The CLI can optionally plot the arc if matplotlib is installed. For quick teaching visuals,
use --plot-raw to show the underlying segment scores alongside the smoothed curve.
For multi-dimensional arcs (e.g., --score-col probs), the CLI will draw multiple lines
with a legend instead of a filled scalar curve.
fabula arc my.txt --plot arc.png
# or show interactively
fabula arc my.txt --plot -
fabula arc my.txt --plot arc.png --plot-rawYou can also create plots programmatically:
from fabula import Fabula, plot_arc
arc = Fabula().arc(\"...\")
plot_arc(arc, subtitle=\"Chapter 1: Opening scene\", raw_points=True, save_path=\"arc.png\")Formats:
csv(default)jsonjsonl
Each row includes:
idx: segment indexrel_pos: relative position in the document (0..1)text: segment textlabel: top predicted labelscore: scalar score for arcsprobs: full label distributionchunk_probs: pooled chunk distribution (in-context mode only)start_char,end_char: character offsets (when available)start_token,end_token: token offsets (window/in-context modes)
Formats:
csv(default):x,yfor scalar arcs, orx+ one column per label for vector arcsjson: scalar arcs includex,y,raw_x,raw_y; vector arcs includex,y_series,raw_x,raw_y_series
raw_x and raw_y are the original segment positions and scores before resampling/smoothing. For vector arcs,
the *_series fields store one list per label.
The CLI has two subcommands: score and arc. Both share common options.
input: text file path or-for stdin-o, --output: output path or-for stdout--encoding: file encoding (default:utf-8)--dummy: use a tiny built-in scorer (no Transformers download)--analysis:sentimentoremotion--model: Hugging Face model ID (ignored with--dummy)--device:cpu,cuda, orcuda:0--batch-size: inference batch size--max-length: max tokens per segment--pooling:none,mean,max,attention--pooling-stride-tokens: stride for pooled chunking (defaults tomax_length/4)--segment:sentence,paragraph,window,in-context--window-tokens,--stride-tokens,--min-tokens: window segmentation controls--chunk-tokens,--chunk-stride-tokens,--chunk-min-tokens: in-context chunking controls--chunk-weight: interpolation weight for chunk scores--chunk-attention-tau: attention pooling temperature for chunk scores
--format:csv,json, orjsonl
--format:csvorjson--n-points: number of resampled points--smooth-window: smoothing window size--smooth-method:moving_average,gaussian, ornone--smooth-sigma: gaussian sigma (defaults towindow/6)--smooth-pad-mode:reflect,edge, orconstant--score-col: column to use as scalar score (default:score; useprobsfor per-label arcs)--score-cols: comma-separated columns for vector arcs (overrides--score-col)--no-fallback-to-maxprob: disable fallback score for missing scalar values--plot: output file path or-to display
Fabula(
scorer,
segmenter=None,
coarse_segmenter=None,
analysis="sentiment",
chunk_weight=0.3,
chunk_attention_tau=0.1,
)Methods:
score(text) -> pandas.DataFramearc(text, n_points=100, smooth_window=7, smooth_method="moving_average", smooth_sigma=None, smooth_pad_mode="reflect", score_col="score", score_cols=None, fallback_to_maxprob=True) -> ArcResult
RegexSentenceSegmenter(pattern=..., min_len=1)ParagraphSegmenter(min_len=1)SlidingWindowTokenSegmenter(tokenizer, window_tokens=256, stride_tokens=64, min_tokens=16)DocumentChunkTokenSegmenter(tokenizer, chunk_tokens=1024, stride_tokens=1024, min_tokens=128)
TransformersScorer(model, device=None, batch_size=16, max_length=512, pooling="none", pooling_stride_tokens=None)
fabula arc my.txt --analysis sentiment --n-points 100fabula arc my.txt --analysis emotion --model astrosbd/french_emotion_camembertfabula arc my.txt --analysis emotion --score-col probs --format csvfabula score my.txt --segment in-context --chunk-tokens 2048 --chunk-weight 0.4fabula score my.txt --dummy --analysis sentiment- The default models are French. For other languages, pass a different Hugging Face model.
windowandin-contextsegmentation require a Transformers tokenizer (disable--dummy).in-contextsegmentation requires a tokenizer that returns offset mappings.scoreoutputsscore=Nonewhen the model labels do not support valence;arccan fall back to max probability unless disabled.
Licensed under the MIT License.