Turns a Facebook Messenger export into a set of reports, charts and web pages, plus a local reader that lets you scroll the whole chat like a messaging app.
Everything runs on your machine. Nothing is uploaded anywhere.
- Quick start
- What you get
- Options
- Common tasks
- The chat reader
- How the harder numbers are worked out
- Privacy
- Examples and sample data
- Tests
Four steps. You need Python 3.8 or newer.
1. Install
pip install -r requirements.txtOr install it as a package, which gives you a chatflashback command:
pip install .Two optional extras add features. Without them the tool skips those sections and keeps going.
pip install ".[full]" # vaderSentiment for mood, wordcloud for word clouds2. Download your Messenger data
- Facebook Settings, then Your information, then Download your information.
- Select Messages and the chats you want.
- Set the format to JSON and the media quality to Low.
- Download the zip and extract it.
The folder you want holds message_1.json, message_2.json and so on. It sits at
youraccount_.../your_activity_across_facebook/messages/inbox/<thread>/.
3. Run it
python analyze_chat.py --input data --output outputPoint --input at one thread folder, or at the whole messages/inbox/ folder to do
every thread in one go.
4. Open the report
Open output/<thread>/report.html in a browser. It is one self-contained file with
every table and chart in it, so you can send it to somebody as is. The other pages
sit next to it and are linked from the bar across the top.
To read the chat itself instead:
python analyze_chat.py --input data --serveThat starts a reader on http://127.0.0.1:8080.
Running the tool writes everything into output/<thread>/.
report.html and summary.md hold the same numbers, one as a web page and one as
plain text. Add --json to also get summary.json.
Inside: yearly recaps with the top member, word and record day of each year. Member
personalities with signature words, favourite emojis, peak hour and night-owl share.
Reaction dynamics. A response-speed leaderboard with median reply time and the share
of each member's turns that nobody answered. Swear-word counts per member. Who starts
conversations, who ghosts, who monologues. Question dynamics: who asks, who answers,
who gets left on read. Emoji counts and a timeline of favourite emojis. Topic words
per year. Repeated phrases that look like inside jokes. Message-length and word trends
over time. Mood per member and per year, if vaderSentiment is installed. A weirdest
statements reel of all-caps, 3am and punctuation-spiral messages. A media leaderboard
for photos, stickers, GIFs, videos, audio and files.
Around 40 PNG charts come with it, including a GitHub-style activity heatmap, pair matrices of who replies to and reacts to whom, hourly radars, word clouds and a sleep-schedule grid of everyone's posting hours year by year.
| Page | What is on it |
|---|---|
year_in_review.html |
An index, plus year_<year>.html for every year |
members.html |
An index of everyone, linking to their pages |
group_history.html |
Every name the group gave itself and every nickname, as dated ranges |
member_<name>.html |
One per member: their years, their words, who they answer, their most-reacted messages |
relationships.html |
Pairs by year, pairs that drifted apart, who breaks the silence, who gets the last word |
eras.html |
The chat cut into periods, each named after the word it made its own |
sessions.html |
Conversations: who opens them, who ends them, how long they run, the longest silences |
trendsetters.html |
Who says a word first and then watches everybody else start saying it |
quiz.html |
Guess who said this, built from each member's signature words |
--serve opens a Messenger-style reader on localhost:
- Infinite-scroll feed grouped by day, newest or oldest first
- Sender colours, inline reactions, reply threading and "N years ago" badges
- Photo and GIF thumbnails, inline video and audio players, download links for files
- Search across every message, with a
.*regex toggle and per-member filters - Jump to a date, an "On this day" view across the years, and a "Surprise me" button
- A word explorer behind the Words button
- Every page above in one navigation bar, in one theme that follows your system until you pick one and is then remembered
- Searches, filters and jumps live in the address bar, so the back button undoes them and a view can be bookmarked or shared
Newer export fields (gifs, videos, audio_files, files, polls, is_taken_down)
are read, and messages that appear in more than one file are de-duplicated.
Copy-paste floods are capped. Each word or emoji counts at most three times per message, so one pasted wall of the same word cannot decide the top words or what a year was about. Volume stats still count every keystroke, and the totals say how many floods there were.
Members that are obviously software, such as Meta AI, are labelled (bot) and kept
out of the human awards.
| Flag | What it does |
|---|---|
--input, -i |
A thread folder or an export messages/ folder (default: data/) |
--output, -o |
Where to write everything (default: output/) |
--anonymize |
Replace names with Person A, Person B everywhere, including inside quoted messages |
--track |
Words or phrases to count and chart, e.g. --track "lol, bro" |
--track-file |
The same, from a file, one per line (# for comments) |
--names |
Names the export does not list, such as a deleted account showing as "Facebook user", so they do not read as topic words |
--stopwords-file |
Extra words to ignore, one per line. The built-in list is English only |
--year |
Analyze one year only, e.g. --year 2017 |
--top |
How many rows in each leaderboard (default: 10) |
--trend-band |
How often a word must be used to count as one somebody started, as min,max (default: 20,2000) |
--json |
Also write summary.json |
--serve |
Start the reader instead of writing reports |
--port |
Port for --serve (default: 8080) |
--no-index |
Skip the word index when serving. Starts instantly, but the word explorer is off |
--tz |
Timezone, e.g. +03:00 or America/New_York. Messenger timestamps are UTC and the default is your system timezone |
--config |
A JSON file holding any of these options |
--skip |
Skip analyses: jokes, sentiment, wordcloud, topics, narratives |
--progress |
Print which phase is running |
--incremental |
Skip threads that have not changed since the last run |
--check |
Validate the export instead of analyzing it |
python analyze_chat.py --input data --anonymizeNames become Person A, Person B in every chart, table and quoted message.
The built-in stopword list is English only. In a chat that mixes languages, the other language's function words take over the top-word, topic and inside-joke sections. A Hinglish list ships with the tool:
python analyze_chat.py --input data --stopwords-file stopwords/hinglish.txtAny file works: one word per line, # for comments. Chat spelling is not standard, so
a word usually needs several entries (mein, mei, mai) before it stops showing up.
The analysis holds everything in memory, so a chat with hundreds of thousands of messages needs headroom. Two phases dominate. Inside jokes counts every 2 to 4 word phrase and drives peak memory. Sentiment is the slowest. Drop both if a run is too heavy:
python analyze_chat.py --input data --skip jokes,sentimentEvery other report, chart and page is still written.
python analyze_chat.py --input data --track "shawarma, bro"Each term gets a row in the report and a line on a chart. --track-file reads a longer
list from a file.
{
"input": "data",
"output": "out",
"top": 15,
"json": true,
"track": "lol, bro"
}python analyze_chat.py --config config.jsonFlags on the command line still win over the file.
--incremental stores a fingerprint of each thread (file names, sizes, modification
times) in output/.chatflashback_state.json and skips threads that have not changed.
Changing a flag such as --year or --top forces a re-run.
python analyze_chat.py --input data --checkPer thread, it reports message types, unknown type values and unknown message keys
so a new export format is easy to spot, plus empty messages, attachments missing from
disk, duplicate messages and gaps of over 90 days between message files. Add --json
to also write check.json. It never analyzes and always exits 0.
python analyze_chat.py --input <thread> --serveThe server listens on 127.0.0.1 only, with no authentication, so it is not reachable
from your network.
If you have already run the analysis, --serve also serves every page it wrote, and
they gain links the files on disk do not have: quoted messages on the Conversations
page and answers in the quiz get an "open" link that jumps the reader to that exact
moment, and each member page links to that member's filtered feed. Open the same files
straight off disk and those stay plain text, since there is no reader to open.
Type a word into the panel behind the Words button. You get total uses and how many messages they land in, a per-member table with a per-1,000-messages rate so a quiet member who says it constantly is not buried under a chatty one, the year it peaked, how often it is sent on its own, whether it pulls more reactions than average, who said it first and how long everybody else took to pick it up, the words that sit beside it more often than chance predicts, and example messages with a button that jumps the feed to that moment.
Matching is exact. bruh does not quietly include bruhh. Spellings that differ only
in held-down letters are listed separately, and a "count spellings together" box folds
them into the totals. Autocomplete suggests words by how often they are used.
Type more than one word and it becomes a phrase, counted only where those words sit
side by side, so full send ignores "send me the full list". Emoji are indexed as
words, so π, ππ and lol π all work. Punctuation and capitals are ignored, and
a phrase can be made entirely of stopwords (the end is a fair question even though
the is not).
"Show all N in the feed" turns the reader into every message holding that word, oldest first. Click one and the feed opens the conversation around it.
The index is built at startup and kept in memory. On a 1.79M-message chat that takes
about 30 seconds and peaks near 180 MB while building, for 140,755 distinct words. A
lookup then takes milliseconds for an ordinary word and up to about 3 seconds for one
of the most common ones, since every statistic is computed on demand over the messages
that matched. Phrases need no index of their own, so in the came back in 1.0 s and
what the hell in 0.04 s on the same chat. Pass --no-index to skip the build if you
only want to read.
Parsed messages go into a SQLite file at output/.reader/<thread>.sqlite3, which
answers the feed, the date jump, "on this day", the random memory and search. The file
is keyed on the same fingerprint --incremental uses, plus --tz and --anonymize,
since both change what gets stored. Anything else rebuilds it.
The difference shows on the second start. With --no-index the export is not parsed at
all: the reader opens the file and serves. Without it, the messages are parsed anyway
because the word explorer needs them in memory. A half-written file is rebuilt rather
than trusted, because the "complete" marker is written last.
Search is a substring scan running inside SQLite, and reports the true match count
along with the first page of hits. It is deliberately not a full-text index: FTS5
matches whole tokens, so searching tube would stop finding youtube.com. On a
500k-row benchmark that is 0 hits against 358,453. The scan costs roughly 60 ms per
500k messages.
Media is served from the export folder only. Paths are resolved inside the thread
directory so nothing outside it can be read, and files stream with Range support so
video and audio can seek. Anything that is not an image, video or audio downloads
rather than rendering, since an export can contain .html or .svg attachments.
Most of the report is a straight count. These are the ones with a rule behind them.
A month opens a new era when the three months from it carry less than half or more than double the messages of the three before it, or when fewer than a third of the previous quarter's top words survive into this one. Quarters too small to have a character of their own cannot open an era, and eras shorter than six months are merged into their neighbour.
Each era is named after the word it uses most out of proportion to the rest of the chat, which is not its most common word. TF-IDF was tried first and named four eras out of five after the chat's single commonest word.
Underneath sits the plainest version of the same story: the words the chat first said and last said in each year.
A pair interacts when one answers the other within an hour, or reacts to their message. Those are counted per year.
A pair has drifted when its share of either member's own interaction moves by more than half between the pair's peak year and the most recent year the export covers end to end. Using the share, rather than the raw count, stops a pair reading as drifted when the whole chat simply went quiet. Pairs under 100 interactions in their peak year are left out.
The page also reports who speaks first after a day of silence, who gets the last word, and whose messages go unanswered, as a share of their own messages so the loudest member does not top the list by volume.
The chat is cut wherever nobody spoke for 30 minutes, the same gap the rest of the report uses.
Openers and closers are given as a count and as a rate per 100 of that member's own messages. The count alone just ranks by who talks most; the rate separates someone who always speaks first from someone who is always there.
Conversation length is given as percentiles rather than an average, because the range runs from two-message exchanges to all-nighters. Underneath sits the inverse: the chat's longest silences and the message that broke each one.
Who introduces vocabulary that other people actually adopt, which is a different question from who talks most.
For every word the chat used between 20 and 2,000 times, the tool takes whoever said it first. The word counts as having caught on only once at least three other members said it too.
Three rules keep the answer honest:
- A word first said in the chat's opening 90 days does not count. An export beginning is not the same as a word being new, and without this rule whoever talked most in month one "starts" thousands of words the chat had been saying for years.
- Bots are left out.
- The count is divided by how much each member says, per 1,000 of their own messages, so a chatty member cannot win on volume. Members under 100 messages are left out, since a rate needs a denominator worth dividing by.
On a small chat nothing reaches twenty uses. Lower the band with --trend-band 3,50
and the words still show even when nobody clears the leaderboard's floor.
A message qualifies only if it uses one of its sender's signature words, so the answer is gettable rather than a coin flip. Messages that name somebody give the answer away and are left out, as are bot messages. The three wrong answers are sampled by message volume, so they are plausible for that era. It is seeded, so regenerating the report does not reshuffle the questions.
Messenger writes its own messages about the group: renames, nicknames, joins and removals. They are dropped from the vocabulary, because otherwise "named the group" ranks as an inside joke said by everyone for years. Read back, they are a record nobody has seen. On a nine-year chat that was 520 group names and 369 nickname changes. Nicknames are shown as dated ranges, since the question people ask is what somebody was called in 2021, not when the name was set.
- All processing happens on your machine.
--anonymizereplaces real names everywhere, including names inside quoted messages.--servebinds to127.0.0.1, so the reader is never reachable from the network.- Text that comes from the export, meaning thread titles, names and message bodies, is escaped everywhere it is rendered, in the reader and in the generated reports.
.gitignoreexcludesdata/andoutput/, so an export cannot be committed by accident.
sample_data/ holds a small synthetic thread, 94 messages across four members and nine
years. Run it to see the output without touching your own data:
python analyze_chat.py --input sample_data --track "shawarma, bro" --json --trend-band 3,50examples/ holds exactly that run, committed so you can look before installing
anything: example_summary.md, report.html, summary.json, every generated page,
and the charts. The band is lowered there because 94 messages never reach twenty uses
of a word.
pip install -r requirements-dev.txt
python -m pytestThe suite takes about six minutes.
MIT





