Skip to content

Bulk media optimisation

Bulk media optimisation fixes alt text (and titles, captions, descriptions and file names) across the whole library in one pass. You export a package, an AI agent running on your own machine looks at every thumbnail and writes the text, and you upload the corrected file back.

The package is exported under Tools → Octa Editor → Export, card Images to describe, and the edited manifest goes back through Tools → Octa Editor → Import. Both need the upload_files capability.

Nothing is sent to a model from your site. The agent runs where you run it, which is what makes this different from the per-image AI generation in the Media Library column.

  1. Export a package. Thumbnails, a manifest to fill in, and the instruction file for the agent.
  2. Work on it locally. Unpack the ZIP, open the folder with your agent and tell it to read AGENT-GUIDE.md.
  3. Upload the edited manifest.json on the Import tab. Review the preview, field by field, then save. The whole package .zip works too, as exported or with its folder zipped again, as long as it holds one manifest and no content documents. A package too large to upload goes back as its manifest.json alone.

Pick the fields the agent should fill in. Alt text, title, caption and description are available to anyone who can open the tab. File name is a separate and much stronger permission, covered below.

Only images with no alt text narrows the package to the images that actually need work. Every image the agent opens costs one look, so on a large library this filter is usually the difference between minutes and hours.

The export runs in slices rather than one long request, showing progress as it packs. The finished ZIP looks like this.

manifest.json
AGENT-GUIDE.md
media/
thumbnails/
123.jpg
124.png

manifest.json is keyed by attachment id, and each entry carries the current state of every editable field plus a new_* slot for each field in scope.

{
"_meta": { "schema": 1, "kind": "octa-editor/media-manifest", "scope": ["alt", "title"], "total": 842, "included": 842, "truncated": false },
"images": {
"123": {
"thumb": "media/thumbnails/123.jpg",
"filename": "IMG_4821.jpg",
"alt": "",
"title": "IMG 4821",
"used_in": [ { "context": "featured", "title": "Bathroom renovation, Gdansk" } ],
"new_alt": null,
"new_title": null
}
}
}

used_in tells the agent where the image is actually used on the site (featured image, page content, Elementor, ACF or custom fields), which is what turns a generic description into one that matches the page. It only ever lists records the exporting user is allowed to read.

A package is bound to the user who produced it, and it is removed automatically after 24 hours. Starting a new export replaces the previous one, so download it before you start another.

The caps bound one package, not your library. When a limit stops the export short, the screen says so and offers Continue where it stopped. Continuing keeps the settings of the previous package and picks up exactly where it left off, so no image is handed over twice and none is skipped.

AGENT-GUIDE.md ships inside every package and tells the agent everything it needs. It asks the agent to open each thumbnail and describe what it actually sees rather than guessing from the file name, it sets the writing rules per field, and it asks you for business context (industry, audience, location, priority keywords) before it starts writing, because without that the output is correct but generic.

The one rule that matters on your side is that null means “leave this alone”. The agent fills in only what it wants to change, and an empty string "" explicitly clears a field.

The guide is written in the site language and is a single-language document by design.

Upload the manifest.json the agent edited on the Import tab. Zipping the whole folder again works too: the importer finds manifest.json at the root of the archive or one folder down. The manifest is recognised by its _meta.kind, so keep the _meta block in the file, and upload it on its own rather than together with content files. Nothing is saved yet.

The preview lists every change, field by field, with the current value next to the proposed one. It also reports how many entries already match the manifest and will be left alone, and how many cannot be applied at all. Only the new_* fields are read. Everything else in the file is ignored, so the agent cannot change identity by editing the rest.

Saving runs in slices, re-checking permissions per image, and reports what was written.

The importer accepts both an id-keyed map and a list where each entry carries its own id. It never guesses. When a map key disagrees with an inner id, the entry is dropped rather than applied to whichever of the two looked more plausible. Changing a key in the manifest means writing to a different image, so leave the keys alone.

After a save you get a Download undo file. That file is the only way to revert the change, so download it.

The undo file is a manifest in reverse, the same ids with new_* set to the values that were there before. Restoring means uploading it through the same import flow, with the same preview. When a rename was part of the change, the undo file moves the files back, rewrites the addresses again and removes the redirect.

Renaming is the riskiest operation in the plugin, because it moves files on disk and rewrites their addresses across posts, custom fields and options. It is therefore gated on its own, stronger capability, manage_options by default, which is checked again at the moment of writing rather than trusted from the preview. Without it the scope checkbox is not offered, the field is stripped from the manifest, and any new_filename in an uploaded file is ignored.

When a rename is in the batch, the preview shows the blast radius per file, counting how many posts, custom fields and options each one rewrites. Back up the database and the uploads folder first, and clear any page cache afterwards.

What happens on save:

  • The main file, the untouched original and every generated size move together as one unit. A partial failure rolls the whole rename back.
  • Addresses are rewritten in a serialization-aware way, so a value stored by a page builder inside serialized data survives intact.
  • The old URL keeps working through a 301 redirect, served only on a 404 inside the uploads folder.
  • The name is rebuilt from scratch with WordPress slug rules, so nothing from the manifest reaches the filesystem verbatim. A name that is already taken is stepped around, never overwritten.

Leave renaming off unless the current file names are genuinely unusable.

Two independent gates apply here. The upload_files capability decides who sees the image export and import. It never decides which images travel.

Collection is author-scoped for a role without edit_others_posts, and every attachment is re-checked individually inside each slice. An author exporting the library gets only its own uploads. A finished package belongs to the user who produced it and cannot be downloaded by anyone else, because handing it over would return exactly the images the scoping withheld.

Script vectors are rejected outright. A manifest containing a script tag, an iframe, an inline event handler or a javascript: link is blocked at parse time, before any database read, for every role including administrators.

A package holds up to 1000 images and 50 MB of thumbnails, and an uploaded manifest may be up to 20 MB. The batch sizes for one request differ between export and import, because importing runs table scans while exporting only copies thumbnails. Everything is filterable through octa_editor_media_limits.

The export needs the ZipArchive PHP extension, and the button is hidden when the host lacks it.

Packages sit in a working directory under uploads/octa-editor/, protected from direct access and reachable only through a capability-checked handler. Uninstalling the plugin removes the folder along with every option and transient the feature uses.