Most of the code was AI-generated from a detailed architecture prompt. I designed the pipeline, the AI wrote the C++. Here's what we built.
I had a problem. My dad takes 1.5 hour long banking regulation classes recorded as screen recordings of someone scrolling through RBI circulars and banking law documents. No download link. No PDF. Just a video of text scrolling past.
I didn't want to type it all out manually.
So instead of spending 3 hours typing, I spent an afternoon designing a solution and prompting an AI to write the C++. The result is Palimpsest β a high-performance CLI tool that extracts text from screen recordings of scrolling documents, deduplicates overlapping frames, and outputs a clean .txt
or .docx
file.
It runs at 323 FPS on a laptop iGPU and gets 94.56% accuracy on dense banking regulatory text.
GitHub: https://github.com/plexescor/Palimpsest
If you've ever needed to extract text from a screen recording, you know the pain:
Existing tools fail here. Generic OCR tools handle single images. Cloud video OCR services (ScreenApp, etc.) charge monthly subscriptions, cap video length, and you're up private content to someone's server. Manual transcription of a 1.5 hour recording is genuinely painful.
There was no good free, local, offline tool that specifically handled the scrolling document problem β deduplicating overlapping frame content and stitching it into a clean linear document.
So I built one.
Palimpsest β a historical term for a manuscript page that has been scraped clean and rewritten over. Exactly what this tool does to a scroll recording.
This is where it gets interesting. The tool isn't just "loop through frames and OCR each one." That would be slow and produce terrible results due to duplicate content across scroll frames.
The architecture is a strictly staged producer-consumer pipeline with lock-free SPSC (Single Producer Single Consumer) ring buffers between every stage:
[Decoder Thread]
β SPSC RingBuffer<RawFrame, 128>
[Frame Filter & Dwell Gatekeeper]
β SPSC RingBuffer<FilteredFrame, 64>
[Preprocessor Thread]
β WorkQueue<PreprocessedFrame>
[OCR Worker Pool β N Threads]
β ResultQueue<OcrResult> (unordered)
[Reorder Buffer]
β ordered stream
[Dedup & Stitch Thread]
β
[Structure Recovery]
β
[Output Writer β TXT or DOCX]
Every queue is bounded. Backpressure propagates upstream automatically β if OCR workers slow down, the work queue fills, the preprocessor blocks, the frame filter blocks, the decoder blocks. RAM stays bounded regardless of video length. A 3 hour video uses the same memory as a 5 minute one.
The most important design decision in the whole tool is something I call dwell gating, and it's what separates Palimpsest from naive frame-by-frame OCR approaches.
Most people's first instinct for this problem is: detect when the frame is changing (scrolling happening) and capture those frames. This is completely backwards.
Here's why. The frames captured during active scrolling are:
What you actually want are the frames where the educator stopped scrolling and is talking about the content β because those frames are:
Dwell gating works by tracking frame stability over time. A frame is only forwarded to OCR when it has been continuously stable for --stable-dwell-ms milliseconds (default 5000ms β five full seconds). During scrolling, the stability counter resets. During s, it accumulates. Only settled, stable, clean frames ever reach Tesseract.
The result: from 21,000 frames in a 5 minute 50 second video, only 5 frames were forwarded to OCR in normal operation. Five perfect frames instead of thousands of garbage ones. 94.56% accuracy on the output.
Even with dwell gating, consecutive captured frames overlap significantly. Someone scrolls down, s β you see the bottom half of section A and the top half of section B. Next β full section B. Next β overlap of B and C.
Palimpsest handles this with fuzzy suffix-prefix stitching via rapidfuzz:
--overlap-window
characters, default 300)--overlap-window
characters)rapidfuzz::fuzz::partial_ratio
to find the overlap boundary--overlap-threshold
(default 75): find exact boundary, append only new content[GAP IN CONTENT]
marker, append full frameA rolling seen-lines history handles cross-GAP deduplication β if someone scrolls back up and the same content reappears later, it's detected and skipped via fuzzy matching.
Two preprocessing backends, selected via --backend
:
CPU (default) β standard cv::Mat
pipeline, Gaussian blur, Otsu threshold, Hough deskew. Fully portable, zero dependencies beyond OpenCV.
OpenCL β same pipeline but using cv::UMat
throughout, dispatched transparently to the iGPU via OpenCV's OpenCL backend. On startup, enumerates devices and prints which one was selected. Falls back to CPU silently if no OpenCL device found.
OCR always runs on CPU regardless of backend β Tesseract's OpenCL path is unreliable and slower on integrated GPUs due to driver overhead.
Real hardware: AMD Ryzen AI 7 350 (8 cores, 16 threads), Radeon 860M iGPU (8 CU, 8GB shared), 16GB DDR5 @ 5600 MT/s, Arch Linux.
Video: 1920Γ1200, H.264 @ 6 Mbps, 60 FPS, 21,000 frames (5 minutes 50 seconds).
Normal operation (dwell mode):
| Backend | Forwarded to OCR | Time | Avg FPS | Real-Time Factor |
|---|---|---|---|---|
| CPU | 5 | 1m 06s | 318 FPS | ~5.3Γ faster than real-time |
| OpenCL | 5 | 1m 05s | 323 FPS | ~5.4Γ faster than real-time |
In dwell mode the bottleneck is purely the video decoder. CPU and OpenCL are identical because almost no frames reach preprocessing.
Preprocessing + OCR stress test (20,000 frames forced through full pipeline):
| Backend | Time | Avg FPS | Per-Frame |
|---|---|---|---|
| CPU | 2m 47s | ~120 FPS | ~8.3ms/frame |
| OpenCL | 2m 41s | ~107 FPS | ~9.4ms/frame |
CPU beats OpenCL in the stress test because the bottleneck is Tesseract (CPU-only) and OpenCL dispatch overhead costs more than the preprocessing savings at this scale.
Accuracy benchmark on banking regulatory text (RBI circulars, committee names, legal language):
94.56% accuracy on a real-world banking regulation document. The errors were: one line missed due to fast scroll (tunable with --min-interval-ms
), one phantom character from compression artifact (discipline
β disciplinec
), minor list prefix heuristic misfires. All fixable in a 5 minute review pass.
./build/bin/palimpsest recording.mkv
./build/bin/palimpsest recording.mkv -b opencl -f docx --crop 20,60,1880,1100 -o output.docx
./build/bin/palimpsest recording.mkv -c config.csv
./build/bin/palimpsest recording.mkv --ignore ignore_phrases.txt
./build/bin/palimpsest recording.mkv -b opencl --noshit
Key flags:
--stable-dwell-ms
β how long a frame must stay stable before OCR (default 5000ms)--crop x,y,w,h
β exclude UI chrome, taskbars, window ribbons--ignore
β strip watermarks and recurring boilerplate--dump-images
β dump the exact frames that got OCR'd, for debugging--bench-preprocess N
β stress test mode, force N frames through full pipelineFull flag reference and a documented config.csv
are in the repo.
No vcpkg. No system package manager. One command builds everything from source:
git clone https://github.com/plexescor/Palimpsest.git
cd Palimpsest
./compile.sh
CMake FetchContent pulls and builds:
Runtime requirement: GStreamer or FFmpeg for video decoding (standard on any Linux with video playback), tessdata in ./tessdata/
or via TESSDATA_PREFIX
.
Almost all of it.
I designed the full pipeline architecture in conversation with Claude β every stage, the ring buffer design, the dwell gating concept, the dedup stitching algorithm, the backend abstraction interface, the DOCX XML structure. Then I wrote a detailed prompt describing the entire system and Claude generated the C++ implementation in one shot.
The AI wrote:
.docx
filescv::UMat
What I contributed was the architecture, the dwell gating insight, the understanding of why motion-based extraction is wrong for this use case, the benchmark methodology, and the real-world test on actual banking lecture content.
The result was 94.56% accuracy on first real-world test. That's what happens when you understand the problem well enough to describe the solution precisely.
If you've ever had any of these problems:
Palimpsest is free, runs locally, never uploads your content anywhere, processes any video length, and takes about a minute for a typical lecture recording.
--stable-dwell-ms
or set it to 0 for motion-threshold mode.
git clone https://github.com/plexescor/Palimpsest.git
cd Palimpsest
./compile.sh
./build/bin/palimpsest your_recording.mkv
Drop the output quality issues you hit in the comments β real feedback on diverse content types helps tune the defaults.
Built with C++17, OpenCV, Tesseract, rapidfuzz, miniz, {fmt}, CLI11. Named after ancient manuscript pages scraped clean and rewritten over.
GitHub: https://github.com/plexescor/Palimpsest