Use Opus 5.5 to turn CSV data into a chart video
Build a CSV-to-video workflow with an Opus 5.5 prompt, validated sample data and a downloadable Python project. Check the numbers before exporting MP4.
To make a chart video with Opus 5.5, give it validated data and a fixed visual specification, then draw the frames with Python and encode them with FFmpeg. Keep the original counts outside the animation. A bar growing across the screen must not quietly become a claim that the underlying business metric grew over time.
This tutorial builds one small, reproducible deliverable: a 12-second, 1280 × 720 MP4 showing three categories from a CSV. It is for people turning reports into presentation or social video who can run a local command. You will prepare the data, use a complete Claude prompt, preview the chart, export it, and check the numbers in the finished file. For the general setup and other video workflows, read the Opus 5.5 video guide.
Watch the reference result and download the project
An actual local render of an editorial reference project. The numbers are synthetic, the labels are English, and the video is intentionally silent. This is not Ofox customer, revenue or support data.
Download the complete project, the MP4, and the complete prompt.
Evidence boundary: the current attempt to call Opus 5.5 through Claude Code failed because the session was not logged in. We therefore wrote the reference implementation editorially and tested its CSV validation and Python/FFmpeg render. We have not labeled it as a fresh Opus output or a successful one-shot generation. You can use the prompt with your own authorized Claude access, or reproduce the supplied project without a model call. A previous, separate screenshot-video tutorial documents its own successful model call; that does not verify this one.
Decide what the chart is allowed to say
The sample is a comparison of ticket counts by channel. It is not a chronological sequence, conversion funnel or growth chart. These distinctions affect both the animation and the narration you might later add.
| Category | Count | Meaning in this example |
|---|---|---|
| 120 | Synthetic tickets assigned to Email | |
| Chat | 180 | Synthetic tickets assigned to Chat |
| Docs | 90 | Synthetic tickets assigned to Docs |
The total is 390 tickets. Chat has 60 more tickets than Email in this fabricated example. That does not establish that Chat converts better, saves time, or produces more satisfied users. There are no denominators or outcome measurements here.
Use a zero baseline and the same 0–200 scale for all three bars. The final widths are 60%, 90% and 45% of the plotting area. A viewer should be able to compare the bars without decoding different scales. Do not truncate the axis just to make the difference look dramatic.
For real data, record the source, extraction date, reporting timezone, unit and aggregation rule alongside the CSV. Check whether categories overlap. If they do, their sum may not represent a unique total. Remove personal data before putting it into a model prompt, and only share data you are authorized to use.
Set up the reproducible environment
The archive contains sample.csv, prepare.py, test_prepare.py, render.py, requirements.txt, prompt.txt and a README. The reference uses Python 3.9 or later, Pillow 11.3.0, and FFmpeg with an H.264 encoder (libx264) available on your command path. A local TrueType font supplies the text. No browser or model API is required to render the supplied project.
Unzip into a local directory and run the commands from there. Create a Python virtual environment using python3 -m venv .venv, activate it with source .venv/bin/activate on macOS/Linux or .venv\Scripts\Activate.ps1 in Windows PowerShell, then install and validate:
python3 -m pip install -r requirements.txt
python3 prepare.py
python3 -m unittest test_prepare.py
ffmpeg -version
The expected preparation message is Validated 3 rows. Total: 390. Stop if it differs. Five test methods cover the valid input, zero/boundary counts, a quoted comma in a label, invalid values and malformed rows. A passing parser is not yet a video render.
Set CHART_FONT to a local, licensed TrueType font. For example, on macOS where this file exists:
export CHART_FONT='/System/Library/Fonts/Supplemental/Arial.ttf'
On Linux, an installed DejaVu Sans may be at /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf. In Windows PowerShell, an installed Arial can be selected with $env:CHART_FONT='C:\Windows\Fonts\arial.ttf'; use your Python command, often python, in place of python3. These are example paths, not a promise that every computer has those fonts. No font is redistributed in the archive. Choose a font with the necessary language glyphs and check its license.
Use your system’s trusted installation method for FFmpeg if the command is missing. The FFmpeg download page lists platform options. Asking Claude to write or revise code requires your own access and may consume your plan allowance; running the supplied Python code does not make a model call. We do not infer a paid service or API compatibility claim from this local example.
Validate the CSV before the model sees it
Use this exact header and sample:
label,value
Email,120
Chat,180
Docs,90
The parser uses Python’s CSV reader, so a quoted label such as "Email, shared" remains one cell. It accepts UTF-8 with or without a byte-order mark. It rejects a renamed header, duplicate or blank labels, extra cells, a different row count, nonnumeric values, negative counts, fractional counts, infinity, and values above the chart’s maximum.
That strictness is intentional. This is a three-category integer-count layout. A missing measurement must not silently become zero. A decimal rate must not be rounded into a count. A value of 240 must not be clipped at 200 and passed off as a correct bar.
To inspect another file, run python3 prepare.py my-data.csv. The helper writes data.json only after all rows pass. For rendering your own data, back up the sample and replace sample.csv, then validate again. The renderer reads and validates sample.csv directly on every run; it never silently falls back to the inspection JSON.
If validation fails, an old out/chart.mp4 may still exist. Do not deliver that old file as if the new render succeeded. The renderer writes a temporary MP4 and replaces the final file only after FFmpeg reports success. Save the successful command output and out/accepted-data.json with your deliverable.
Labels are limited to 24 characters. The renderer also rejects labels wider than 200 pixels in the chosen font rather than letting them collide with the chart. Shorten with an explained abbreviation, or deliberately redesign the label column. Character count alone does not guarantee a good fit, and glyph availability still needs visual review.
Give Opus a complete, bounded animation brief
Attach the validated rows or paste them with the specification below. The full downloadable prompt includes the positions, validation rules and typography used in the reference layout. This shorter task brief is useful when adapting the project you already downloaded:
Edit this existing Python/Pillow and FFmpeg project to make a 12-second chart video.
Read sample.csv with the existing prepare.py parse() validator.
It contains exactly three rows: Email 120, Chat 180, Docs 90.
These are synthetic ticket counts, not real company results.
Keep output at 1280x720, 30fps and 360 RGB frames.
Keep a zero baseline and a shared, fixed maximum of 200.
Keep the exact numeric labels visible and unchanged at every frame.
Reveal bar widths only between frames 30 and 90; hold thereafter.
Use progress=max(0,min(1,(frame-30)/60)) for the reveal.
Do not use random values, network data or new packages.
Keep a permanent label: Synthetic example · tickets · fixed scale 0–200.
State that the reveal is not growth over time.
Preserve CSV validation and do not substitute zero for missing values.
If a value exceeds the maximum, stop and explain; do not clip it.
Return the changed file, explain changes, and give the render command.
Do not invent a test result. List tests that still need to be run.
Review the answer before executing it. Compare its package changes and file operations with your request. A proposed installation, data upload or overwrite is a separate action; it is not required merely because generated instructions include it. For this small chart, the existing dependency set is enough.
If you want a different story, change the brief first. “Make it more exciting” gives the model permission to add motion without explaining what must stay true. A better request is: “Keep every value and the scale; use a two-second introduction, then hold the finished chart for at least six seconds.”
Understand the timeline and check the fixed scale
At 30 frames per second, 360 frames make 12 seconds. The reference holds the empty bar area for the first second, reveals the bars over the next two seconds, then leaves the completed chart readable for the remaining nine seconds.
| Frames | Time | Expected image |
|---|---|---|
| 0–29 | 0–under 1 s | Title, labels, exact values and axis are visible; bars have zero width |
| 30–89 | 1–under 3 s | Bar widths reveal; exact numeric labels stay unchanged |
| 90–359 | 3–under 12 s | All bars remain at their final lengths |
The core calculation is small:
progress = max(0, min(1, (frame - 30) / 60))
width = round(value / 200 * 820 * progress)
The plotting area is 820 pixels wide. At the final state, Email is 492 pixels, Chat is 738 pixels and Docs is 369 pixels. At frame 60 the reveal is halfway, subject to pixel rounding, but the labels still show the original data. This animation shows an entrance, not an intermediate measurement.
The clamp keeps progress between zero and one. Without it, a linear calculation can produce negative widths before the reveal or oversized bars afterward. For data graphics, an overshooting animation can suggest values larger than the actual measurement. The reference deliberately avoids that effect.
Export the MP4 and inspect the actual file
After validation succeeds and CHART_FONT is set, render:
python3 render.py
The script draws 360 RGB frames with Pillow and pipes them to FFmpeg. It uses an argument list, not a shell command assembled from CSV input. The encoder settings are libx264, yuv420p, no audio, and +faststart. The FFmpeg documentation describes its input/output and encoding options; the Pillow drawing reference documents the frame drawing primitives.
The expected result is out/chart.mp4, plus four review frames and a copy of the accepted data. Test playback in the destination application. A successful process exit does not prove the text is readable or the interpretation is honest.
If FFmpeg tools are installed, check the output independently:
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,width,height,r_frame_rate,nb_frames \
-show_entries format=duration -of json out/chart.mp4
For the unmodified sample, expect H.264, 1280 × 720, a 30/1 frame rate and a 12-second duration. Frame-count metadata is not guaranteed for every container; do not interpret an absent field as zero frames. Play the beginning, reveal, and final hold. Confirm that the three final numbers are 120, 180 and 90 and that the scale still says 0–200. This sample contains no audio track by design.
Fix problems without changing the meaning
| Symptom | Check first | Appropriate fix |
|---|---|---|
prepare.py reports an invalid value | Missing cells, decimal rates, commas used as thousands separators | Correct the data format; do not silently coerce missing data to zero |
| A value is above 200 | Whether it is a genuine count or wrong unit | Set a new shared maximum in validation, chart widths, ticks and caption together |
| New CSV numbers do not appear | Whether you replaced sample.csv in the working directory and the render succeeded | Validate that file, rerun the renderer, and inspect the newly written MP4 and out/accepted-data.json |
| The renderer rejects a wide label | Label width in the selected font and target language | Shorten with an explained abbreviation, or redesign the label column and recheck the bar area |
ffmpeg or libx264 is missing | Command path and installed encoder support | Install an appropriate FFmpeg build from a trusted source; rerun the version check |
| The MP4 is silent | Whether the composition includes audio | Silence is expected here; add audio deliberately using the linked sound workflow |
| The bars keep growing | Progress bounds or an alternative animation added by a model | Restore the clamped frame-based progress calculation |
A larger dataset requires a design decision, not just a longer array. Ten categories may need a taller layout, grouping or multiple scenes. Negative values need a diverging axis. Percentages need an explicit denominator and formatting rule. None of those changes are validated by this three-count example.
Deliver the data and the video together
Package the accepted CSV, source project, final MP4 and a short note stating the source, units, scale, export settings and whether the data is real or synthetic. Keep the visible synthetic label when sharing this sample. Do not replace it with a company logo and let viewers infer that the numbers are business results.
For a narrated version, see the voiceover and subtitle synchronization workflow. That companion uses a different Remotion project; its commands are not drop-in commands for this Python archive. For a vertical deliverable, see the landscape-to-vertical layout tutorial; changing the dimensions without rearranging labels will make this chart hard to read. If your next video needs product context rather than data, the real-screenshot demo project is the relevant starting point.
The useful role of the model is to help write and revise the animation. The accepted CSV, explicit scale and inspection of the rendered file are what keep the final explanation trustworthy.
Frequently Asked Questions
- Does Opus 5.5 generate the MP4 directly?
- In this workflow Claude helps write or edit code. Python draws frames and FFmpeg encodes the video. The downloadable reference project was prepared by the editor and tested locally; it is not presented as a new Opus-generated result.
- Can I replace the CSV with my own numbers?
- Yes, after checking the data contract. This example accepts exactly three unique labels and integer counts from 0 to 200. Change the layout and scale explicitly before using more rows or larger values.
- Why do the numeric labels stay still while the bars grow?
- They show the actual counts throughout the reveal. The animation is an entrance effect, not a time series or evidence of growth.


