Columnist
A foundation model for osu!mania 4K charts.
It reads a chart as a sequence of note rows, learns from tens of thousands of unlabelled charts, and can then tell you what is in a chart, how hard it is, and which other charts play like it.
What it does
osu!mania charts are long streams of notes, and the things players care about (jacks, streams, long-note coordination, scroll-speed tricks, how hard it feels) are hard to read off with fixed rules. Columnist first learns what charts look like in general, with no human labels, and is then adapted to specific jobs:
| Job | What you get | Try it |
|---|---|---|
| Find similar charts | A 256-number fingerprint per chart. Close fingerprints mean charts that play alike. | Similar charts |
| Pattern labels | A probability for each of 31 labels: 11 pattern categories (Chordjack, Streams, Jumpstream/Handstream, Technical, Stamina, Release, Coordination, Density/Inverse, Reading, Memo, TB) and 20 osu! user tags. | Difficulty & labels |
| Difficulty | A star-like difficulty, a hardest-section indicator and a per-row difficulty curve. | Difficulty & labels |
| Pattern readout while playing | Per-note pattern scores shown live as a chart plays. | Player |
The model only sees note times in milliseconds, long-note ends and visible scroll-speed changes. It uses no snap or BPM-grid information, so live-timed maps work as well as grid-timed ones.
Files
| File | Purpose |
|---|---|
columnist-embed.pt |
Pretrained encoder (256 dimensions, 6 layers, 6.3M parameters), trained only to model charts themselves. Best choice for similarity and retrieval. |
columnist-base.pt |
Pretrained encoder with the same architecture that also learns to predict whole-chart summary statistics. Best starting point for fine-tuning on your own labels. |
joint/ |
The fine-tuned model for labels and difficulty (PyTorch weights, calibration to stars, inference code and an example script). See joint/README.md. |
numpy/joint.npz, numpy/joint.json |
The same fine-tuned model as plain NumPy weights, with no PyTorch needed. This is what the website runs. |
columnist.py, embed_infer.py |
Model code and chart embedding helper. |
chart_tokens.py, prep_dataset.py |
Turn a 4K .osu file into the model's input arrays. |
*.config.json |
Architecture and pretraining settings. |
Quick start
Chart similarity (needs torch, numpy, pandas):
from pathlib import Path
from embed_infer import load_encoder, embed_chart
import chart_tokens, prep_dataset
mode, keys, notes, sv = chart_tokens.parse(Path("chart.osu"))
tok, gap, sv_row, _ = prep_dataset.chart_rows(notes, sv) # 4K mania charts with at least 64 rows
enc = load_encoder("columnist-embed.pt")
vec = embed_chart(enc, tok, gap, sv_row) # 256-number fingerprint
# cosine similarity between two fingerprints = how alike the charts play
Labels and difficulty:
import json, joint_infer
from osu_to_arrays import osu_to_arrays
model = joint_infer.load("joint_plain.final.pt")
out = model.predict(*osu_to_arrays("chart.osu"))
cal = json.load(open("calibration_joint_plain.json"))["linear"]
stars = cal["intercept"] + cal["slope"] * out["difficulty"] # about 3.886 + 0.140 x score
(Run these from inside joint/, or see joint/example.py for a complete script.)
How a chart is represented
Each chart becomes a list of rows, one per moment where a note starts or a long note ends. A row stores which of the four columns have a tap, a long-note head or a long-note tail, the milliseconds since the previous row, and the scroll-speed multiplier in force. Charts are processed in windows of 512 rows with local attention, so the model never looks more than about 64 rows away from the note it is reading.
Training data
- Pretraining: 62,161 unlabelled 4K charts (59,361 unique) from mappers with a ranked or tournament map. Only chart ids are shared; the charts remain the property of their mappers.
- Fine-tuning labels: 8,453 labelled charts, with labels from osu! user tags, tournament mappool slots and tournament staff sheets. See the dataset.
Results
Evaluated with 5-fold cross-validation where all charts of a song stay in the same fold. AP is average precision, averaged over the 11 categories and over the 20 user tags.
| Model | Categories AP | Tags AP |
|---|---|---|
| Columnist fine-tuned for labels | 0.703 | 0.581 |
| Columnist joint model (labels and difficulty in one network) | 0.698 | 0.578 |
Difficulty: the score correlates with official star rating at Spearman 0.94. After a linear calibration the mean absolute error is 0.33 stars. On held-out tournament mappools, it puts a pair of charts from different rounds in the right order 95% of the time.
Similarity: with columnist-embed.pt, a chart's other difficulties from the same song appear among its 5 nearest neighbours 74% of the time (recall@5).
Hand-built statistics fed to gradient-boosted trees score higher on the categories (about 0.75 / 0.60). Columnist trades some of that accuracy for a single network that also serves similarity, difficulty and per-note readout.
Limitations
- 4K only, and at least 64 rows per chart.
- Long-note patterns are the weakest area; rare labels such as Memo have few examples.
- Labels come from user tags and mappool slots, so the outputs say "would players tag this chart this way", not "this pattern is definitely present". Probabilities are good for ranking charts, not calibrated.
- The difficulty score is learned from chart ordering: use it as a star-like estimate (best between roughly 1 and 11 stars), not as an official rating.
- Trained on charts up to October 2026; very new styles may be scored unreliably.
- This is a research tool, not an official osu! tool, and it should not be used on its own for Beatmap Nominator decisions.
Licence and credit
Weights, labels and this card are released under CC BY 4.0, the code under Apache-2.0. Charts are not redistributed. Use of osu! data complies with fair research use.
Komi. Columnist: a foundation model for osu!mania charts. 2026.