In March 2024, we ended “Implementing a Real-Time Watermark Preview with Fabric.js” by creating a much bigger job for ourselves.
That article was originally about something quite specific: replacing the Dashboard’s slider-only watermark controls with an editor where users could drag, resize, and preview a watermark directly. Fabric.js described the watermark’s position, dimensions, text, and color in the frontend, while libvips generated the final image on the backend. Once the two sides were close enough to what-you-see-is-what-you-get, we ran into a larger problem. There was not much material about libvips online, even less about govips, and Chinese-language documentation was especially fragmented.
So we ended the article with a plan:
We plan to write a practical guide to libvips. It will cover the fundamentals and include examples in Go, Python, and JavaScript. Because the task is complex and everyone on the team has a full-time job, it may take quite a long time.
We also said that an initial version would be online within two weeks. In hindsight, that was clearly too optimistic. Two weeks later, it had not become a complete guide. Two years later, we had published only a collection of separate posts about govips, cropping, gradients, filters, CGO, and image formats.
Good news is that until two and a half years later, in October 2026, this gap is still not filled, so we finally completed the VIPS Handbook: a practical guide to the libvips ecosystem in both Chinese and English, covering govips, sharp, pyvips, and the vips CLI.
The interesting part is not that “AI finished an article we had postponed for two and a half years.” If all we had done was hand an outline to a model and copy the result into dozens of Markdown files, the outcome would not be more useful than the mass of AI-generated material already on the web.
What changed was closer to this: we rethought what a documentation site might need to include, and how AI should replace some of our labor (not thinking) here, and tried to find the value of different models at different stages in the actual work.
Nor did we find one model that was best from beginning to end. What ultimately worked was a pipeline made up of 5.6 Sol High, DeepSeek v4.1 flash, Gemini 4, OpenCode, automated checks, and human judgment.
In 2024, Markdown Was Not the Hard Part
Looking only at the finished pages, technical documentation can appear to be a collection of headings, explanations, code blocks, and images. Once you start producing it, however, you discover that nearly all the expensive work is hidden behind the page.
Take an ordinary resize operation. We first had to identify the relevant operations in libvips, then find the corresponding entry points in govips, sharp, and pyvips. Their names or parameter units might differ, and a binding might combine several low-level operations into one higher-level API. If a binding did not expose a convenient wrapper, we had to keep reading its source to determine whether the capability was genuinely unsupported or merely lacked a helper.
Only then could we write the code. Every language example needed to read the same input.jpg, perform as nearly as possible the same product-level operation, and produce an image that could be compared with the others. Output dimensions, channel count, alpha handling, color space, metadata, and encoding settings could all affect the result. Images that looked similar were not necessarily behaviorally equivalent, and code that ran successfully did not prove that the defaults matched.
After that came screenshots, explanations of the differences, error handling, translation into the other language, and the continuing task of ensuring that the code printed in the article did not drift away from the code that had actually been executed.
The straightforward workflow in 2024 therefore looked roughly like this:
- Find the operations supported by several bindings and take their intersection.
- Research the documentation, source code, and existing examples for each operation.
- Write implementations in Go, Python, and JavaScript.
- Run each experiment against the same input image.
- Compare the outputs and record the differences.
- Turn the process into an article, then produce the other language version.
The workflow was clear, and no individual step was exceptionally difficult. The problem was that it consumed an extraordinary amount of uninterrupted time. Every topic repeated the same cycle of research, coding, execution, screenshots, explanation, and translation. If one binding behaved differently from what we expected, the current chapter could turn into several hours of source-code archaeology.
The old method was not wrong. On the contrary, many of the rules we later designed for the VIPS Handbook came directly from lessons learned in those manual experiments. Image-processing documentation cannot stop at a plausible-looking code sample. The displayed result must actually be generated by that code. And when different bindings achieve the same product goal, that does not mean their APIs or default semantics are identical.
The real change was that, by 2026, we could turn those lessons into rules first and then ask models to apply them repeatedly, greatly reducing the amount of human labor required. However, we also encountered some new challenges.
First, 5.6 Sol High Designed the Track
We did not begin with the first article, nor did we ask a model to “write a complete libvips tutorial.” The first step was to have 5.6 Sol High plan the project as a whole.
That plan needed to resolve a set of interdependent questions: which frontend framework to use, how to keep Chinese and English routes symmetrical, how to classify topics, where code and generated images should live, how several bindings should appear on one page, how experimental results should enter the prose, and which checks could stop an incomplete page from being treated as finished.
The resulting site uses Astro and Starlight. Chinese and English live under /zh/ and /en/, with matching content identifiers and page structures. Page components read from Recipes that have actually been executed instead of duplicating their code in the prose. Wherever possible, every observable image-processing behavior is connected to an input, a program, an output, and result metadata.
We defined a content contract alongside the page structure. In the new pipeline, a request such as “write a resize tutorial” became a task closer to this:
- Who is the page for, and what problem should it solve?
- Which concepts must the reader understand first?
- Which questions must it answer, and which claims must not be made without evidence?
- Which official documentation, source code, issues, and version information may be used?
- What entry points do govips, sharp, pyvips, and the vips CLI provide?
- Which input image should be used, and what output format, dimensions, and channels are expected?
- Which results can be compared pixel by pixel, and which can only be compared at the level of product intent?
- Which structure, warnings, and examples must remain aligned between the Chinese and English pages?
- When a binding does not support something, should the page offer an alternative or explicitly say
Not directly supported?
This did not look like “writing content,” but it was the prerequisite for expanding the content at scale. Given a broad goal, a model can easily produce a well-structured explanation whose facts merely sound plausible. Given explicit inputs, sources, outputs, and acceptance criteria, it behaves more like an engineering executor whose work can be checked.
In other words, we first defined what “done” meant. Only then did we begin generating anything.
DeepSeek v4.1 flash: Expanding the Project
The first large-scale execution pass was assigned to DeepSeek v4.1 flash, deployed on our internal NVIDIA RTX 6000D cluster. We used OpenCode to let it work directly in the repository.
“Work” here did not mean producing an article in a chat window for a person to paste into the project. The model had to read the existing directories and constraints, search documentation and source code, create or modify examples, run commands, inspect outputs, generate Chinese and English pages, and continue fixing its work in response to failed checks.
This led to one of the clearest observations from the project: a model does not have to be exceptional in every dimension to complete a large amount of concrete work. If the task boundaries are clear, the relevant context is available in the repository, and every step has executable feedback, a relatively ordinary model can still be highly productive.
For example, it did not have to guarantee from memory that all four entry points for a Recipe were correct. It could locate the APIs for the relevant versions, write the programs, execute them, and let the Schema, Go tests, output metadata, and site build reveal which constraints had not been met. It could then read the errors, modify the repository, and run the checks again.
The biggest difference from the old process was not the speed of a single generation. It was that research, coding, execution, and correction could happen continuously within one task. Work that once required a person to switch constantly between a browser, editor, terminal, and article could now be organized into relatively stable execution units.
But we encountered a major problem: the first round of prose produced by DeepSeek sounded unmistakably AI-generated.
An individual paragraph was usually grammatical and appeared to contain the necessary information. Read dozens of pages in succession, however, and the patterns became obvious. Sections liked to open by announcing their own importance, then end by summarizing what they had just said. Warnings and limitations repeatedly used the same sentence structures. The Chinese mixed translationese with unnatural category names. Every page was orderly, but that orderliness made the entire site feel like the same template being filled in again and again.
Here is a real revision from the project. The first draft described the Handbook’s scope like this:
The Handbook explains a shared libvips mental model through four commonly used interfaces—govips, sharp, pyvips, and the vips CLI. It covers quick start, fundamentals, practical recipes, binding guides, production considerations, troubleshooting, and reference pages, with every page available in both Simplified Chinese and English.
After Gemini 4 rewrote it, the passage became:
The Handbook explains a shared libvips mental model through four common entry points—govips, sharp, pyvips, and the vips CLI. It covers getting started, fundamentals, real-world use cases, production practices, language bindings, troubleshooting, and reference material, with every page available in both Simplified Chinese and English.
Rendered in English, this is not a dramatic rewrite, and the facts barely change. The distinction is clearer in the Chinese source: 接口 (“interface”) became 入口 (“entry point”), a better umbrella term for both libraries and a CLI, while literal category names such as “practical recipes,” “production considerations,” and “reference pages” were replaced with more natural section names.
Across the headings, introductions, paragraphs, and callouts of dozens of pages, adjustments like these made a substantial difference. Removing the “AI voice” was not a matter of adding one more prompt that said “write naturally.” It required a genuine editorial pass: reading at site scale, deciding what was repetitive, identifying unnatural terminology, and finding paragraphs that were correct but did not help the reader understand the problem any faster.
Gemini 4: From Prose Editor to Repair Engineer
Gemini 4 initially entered the project to solve the prose problem.
It modified the Chinese and English pages directly in the repository. Rather than replacing a few adjectives, it reorganized paragraphs, removed formulaic openings and conclusions, and standardized section names while preserving code, warnings, sources, and version boundaries. After editing, it also had to run the relevant checks so that an apparently harmless rewrite would not break matching heading levels, Recipe references, or internal links across the two languages.
As the project progressed, Gemini’s role expanded from editing to repair. It began handling code and content issues found during review, regenerating outputs, adding tests, and maintaining the related pages. It was no longer a model that polished language at the end of the process; it became the main executor in the rework loop.
This also led us to abandon an appealing idea: choose the “strongest model” first and let it handle everything from project planning through final review. That would have been too expensive, would have repeatedly run into Codex’s five-hour limit, and does not even account for Codex’s habit of quietly switching the underlying model.
In this project, 5.6 Sol High was better suited to designing the overall structure and rules. DeepSeek v4.1 flash was effective at expanding tasks along that predefined track and could later switch roles to look for problems during review. Gemini 4 was better at rewriting, integrating, and repairing work from a clearly scoped issue list. This was not a simple ranking of capability; the models provided different kinds of value at different stages.
Human: define goals and acceptance criteria
│
▼
5.6 Sol High: overall planning
Framework, routes, content contracts, experiments, checks
│
▼
DeepSeek v4.1 flash: first execution pass
Research, code, experiments, bilingual pages
│
▼
Gemini 4: editing
Rewrite, integrate, and run relevant checks
│
▼
DeepSeek + OpenCode: site-wide review
Inspect pages, code, generated artifacts, and technical semantics
│
▼
Human: confirm issues, set priorities, route rework
│
▼
Gemini 4: repair
Fix code and prose, regenerate outputs, add tests
│
▼
Full automated verification + human final review
More importantly, the process did not become three models automatically assigning work to one another. A model could report a problem, but people still decided whether it was real, how severe it was, and who should address it. We retained control over task routing and publication because only we knew the goals of the release, and only we were accountable for the public result.
Cross-Model Review Was More Valuable Than Self-Review
Asking a model to inspect its own output is useful, but it can easily preserve the assumptions under which the output was generated. A piece of code often looks the way it does because the model has already accepted some premise. Asking the same model to review it immediately in the same context does not guarantee that it will question the premise itself.
After the first content pass and Gemini’s large-scale rewrite, we therefore assigned DeepSeek a dedicated site-wide review through OpenCode. Its goal was no longer to “finish this chapter.” Instead, it treated the pages, code, generated artifacts, and the relationships between them as objects to inspect.
This change of role was revealing. The model that had generated much of the first draft became the reviewer, while Gemini, which had edited the prose, took over repairs after people confirmed the findings. Model roles were not fixed, and the model used for generation was not inherently subordinate to the model used for editing.
The review results did not flow automatically to Gemini. DeepSeek first submitted its findings. A person then decided whether each was a technical issue, a documentation problem, or a false positive. Only after confirmation did we turn the scope, expected result, and acceptance criteria into a rework task for Gemini. Gemini updated the code and prose, regenerated the outputs, ran the checks, and then a person reviewed the overall result again.
In this loop, the human task was no longer typing every sentence. It was repeatedly answering five questions:
- What does “done” mean for this work?
- Which model is best suited to the current stage?
- Is the review finding supported by evidence?
- What evidence will show that the problem is gone after rework?
- Can we publish this result and take responsibility for it?
A successful build cannot answer those questions automatically. The following premultiply issue is the clearest example.
A Real Example: Why Double Premultiplication Passed the First Checks
The Handbook’s watermarking and compositing examples needed to handle semi-transparent layers with alpha channels. In the initial implementation, the code explicitly called PremultiplyAlpha() before Composite:
if err := textImg.PremultiplyAlpha(); err != nil {
return err
}
if err := pill.Composite(textImg, vips.BlendModeOver, 26, 16); err != nil {
return err
}
At first glance, this seems reasonable. Image compositing frequently involves premultiplied alpha, and many resources warn that color channels and transparency must be handled correctly before blending. The program compiled, the Recipe ran, the WebP file was exported, and the watermark was visibly present on the page.
The problem was that govips’s Composite ultimately calls vips_composite2, whose default here is premultiplied=false. That does not mean libvips skips premultiplication. It tells libvips that the input uses straight, or unassociated, alpha. The operator premultiplies the inputs internally, blends in premultiplied space, and unpremultiplies the output.
If the caller has already run PremultiplyAlpha() but still lets Composite treat the image as premultiplied=false, the same semi-transparent pixels are multiplied by alpha a second time and the result becomes darker.
DeepSeek found this during the site-wide review. After a person confirmed the problem, we assigned the repair to Gemini: remove the unnecessary premultiplication, correct the explanations in the govips and pyvips examples, regenerate the reference outputs, and add a regression test that did not depend on visual judgment.
The difference on a real page is not always dramatic, which is precisely why screenshots alone are insufficient. The new test constructs a pure white background and a pure white overlay at 50% alpha. Correctly compositing translucent white over white should still produce a value close to 255. With double premultiplication, it is darkened to approximately 191.
if err := base.Composite(overlay, vips.BlendModeOver, 0, 0); err != nil {
t.Fatal(err)
}
if err := base.Flatten(&vips.Color{R: 255, G: 255, B: 255}); err != nil {
t.Fatal(err)
}
raw, err := base.ToBytes()
if err != nil {
t.Fatal(err)
}
if len(raw) == 0 || raw[0] < 254 {
t.Fatalf("expected straight-RGBA composite of 50%% white over white to stay ~255, got %d", raw[0])
}
This case does not prove that one model necessarily understands libvips better than another. It proves something else: using different task contexts for generation, editing, and review, then having a person confirm the finding and route the repair, is more dependable than letting one model declare itself finished within a single session.
It also establishes a simple boundary: compiling, building, and rendering successfully do not prove that the technical semantics are correct. A visual result can help a person notice an anomaly, but a minimal semantic test turns a one-off repair into a constraint that remains effective in the future.
What We Built Was Not a Batch of Markdown Files
In less than a week, the VIPS Handbook had 67 public pages in Chinese and another 67 in English, along with 13 executable Recipes. Its scope had expanded beyond the basic operations imagined in 2024 into getting started, fundamentals, real-world use cases, production practices, language bindings, troubleshooting, reference material, and project information.
Each topic compares four common entry points: govips, sharp, pyvips, and the vips CLI. Govips is the primary reference implementation, but the pages do not assume that other bindings have identical helpers, defaults, or lifecycles. Capabilities that cannot be supported directly should be described honestly, and cross-binding comparisons that remain unfinished should not be presented as verified equivalents.
The results shown on every Recipe page come from programs that were actually executed. Inputs have stable identifiers and documented purposes. Programs produce the original images and metadata, and site components place the executed code and generated results into the prose. This prevents the documentation sample and the runnable program from evolving independently: when an example changes, its output and page must be verified together.
Around that content, the project established checks considerably stricter than asking whether a Markdown file exists:
- Toolchain versions and dependencies must match the locked configuration.
- Recipe Manifests must pass Schema validation and point to real entry files.
- Chinese and English pages must have matching
contentIdvalues, heading levels, warning order, and Recipe relationships. - Fixtures and generated assets must pass provenance and path audits.
- Go examples must pass tests, including checks that image references are closed correctly.
- Reproducible benchmarks and generated results are checked for drift.
- Astro type checking and a complete site build must pass.
These checks cannot prove that every technical statement will remain correct forever, nor can they replace maintainer review. They can, however, move many common failures from “a reader discovers it after publication” to a point when the model is still modifying the repository.
We consider the current VIPS Handbook a complete and usable project, not a preview page waiting for every possible future topic to be written. At the same time, the Handbook will not be frozen. New libvips releases, binding capabilities, and production problems will continue to appear, and the content Registry already reserves space for future expansion. Completing a site and ending maintenance of a handbook are two different things.
The Site Is Live. Why Is the Repository Still Private?
The VIPS Handbook is already usable, but the GitHub repository behind it is still private. That does not mean we intend to publish only the final generated pages. As we explained in libvips Discussions #5255, our ultimate goal is to open-source the entire repository, including the documentation source, runnable examples, and verification pipeline.
We wrote in that discussion:
Our ultimate goal is to open-source the entire repository—including the documentation, runnable examples, and the verification pipeline. Right now, the project is still under heavy active development and the repo contains quite a bit of messy internal scaffolding, draft notes, and work-in-progress tooling. Once the structure settles down and we finish cleaning up the internal clutter, we will make the GitHub repository public.
“Cleaning up” does not mean erasing the evidence that AI participated in the project (of course, we removed many update, bump etc. commit messages), nor does it mean waiting until every possible future topic is complete. A website can be released as a complete handbook while its maintenance repository still needs to meet another set of standards: people outside the project should be able to understand its directories and commands, obsolete drafts should not be mixed with active designs, internal development conventions should be separated from instructions that contributors actually need, and the verification process should be reproducible in a clean environment.
This matters especially to us because public pages let readers see the result, while public runnable examples and a verification pipeline let others reproduce experiments, challenge conclusions, locate regressions, and understand how the pages were generated and checked. We do not want to dump an internal working directory onto GitHub and transfer the cost of understanding it to the community.
Until the repository is public, the Discussion above also serves as an open channel for feedback. Once cleanup is complete, the documentation, experimental code, and verification rules will be released as one project rather than as a Markdown snapshot detached from its production process.
What Actually Changed About Building Documentation in the AI Era?
In one sentence: we used to spend our time producing each piece of content; now we spend more of it designing the system that produces the content.
The old costs were research, coding, trial and error, screenshots, translation, and expanding the site page by page. Those jobs have not disappeared, but a substantial portion can now be performed continuously by models working inside a repository and a feedback loop. Human time moves toward defining content and experimental contracts, selecting the right model for each stage, building repeatable checks, deciding what deserves to be trusted, and turning review findings into rework tasks with testable outcomes.
The project left us with several more specific conclusions about “using AI.”
First, no model is naturally suited to every stage. A model that produces an elegant overall plan may not be the best executor for dozens of consecutive chapters. A model that rapidly expands code and content may write conspicuously formulaic prose. A model that rewrites naturally still needs explicit factual boundaries and automated checks.
Second, model roles can change. DeepSeek produced much of the initial content and later found technical problems as a site-wide reviewer. Gemini began as a prose editor and later took on code repair and maintenance. Fixing them permanently into categories such as “smart planner” and “cheap executor” would have hidden that flexibility.
Third, prompt wording alone cannot remove the AI voice. When dozens of pages share similar structures, transitions, and conclusions, asking for one page to “sound more natural” does not solve the site-wide problem. A person or another model must read across the whole site, restructure information, remove repetition, and perform real editorial work.
Fourth, cross-model review is more reliable than self-confirmation. Changing the model, task objective, and context often exposes assumptions more effectively than asking the original model once more to “check carefully.” The premultiply issue was an example of exactly this.
Fifth, automated checks do not replace human review; they preserve human attention for the things machines cannot judge easily. Paths, Schemas, bilingual structure, dimensions, channels, and test results are well suited to automation. People remain responsible for whether an explanation is honest, a comparison is fair, and a topic actually solves the reader’s problem.
This is therefore not a story about replacing the work of three people with several models. We still had to decide why the Handbook should exist, whom it should serve, which claims could not be published without evidence, and whether a newly discovered problem justified delaying a release. What changed is that a team of the same size can now build a longer, denser, and more repeatable production and review pipeline.
Returning to That Sentence from Two and a Half Years Ago
In 2024, we said that this guide might take a very long time. That judgment was not wrong. If we had continued with the old workflow—finding every API, hand-writing four implementations, running every experiment, and assembling every page in two languages—it might still be sitting on a backlog today.
Two and a half years later, the goal has not changed. We still want to provide clearer and more verifiable material about libvips, especially govips. The code still needs to run, the images still need to be genuinely generated, and differences between bindings still need to be described honestly.
What changed was how we reached the goal. We used 5.6 Sol High to design the framework and rules, DeepSeek v4.1 flash on our internal 6000D cluster through OpenCode to expand the project, and Gemini 4 to edit and repair it. Then DeepSeek returned in a different role for a site-wide review, while people confirmed findings, assigned rework, and took final responsibility.
What emerged was no longer the single “libvips operations guide” we had imagined. It was a practical handbook that can continue to be executed, checked, repaired, and maintained. The entire process can be completed through automation, but each step needs someone to make decisions and take responsibility.
If you use libvips, govips, sharp, or pyvips, visit the VIPS Handbook. And if an explanation, code sample, or experimental result is still wrong, please tell us. Rather than pretending that technical documentation can be finished once and remain correct forever, we would prefer this Handbook to become more dependable with every review and repair.


