feat(rpgmaker): add image translation skill functionality

- Introduced a new button to copy a bitmap-localization skill for editable images, allowing users to easily paste it into coding agents.
- Implemented logic to handle the copying of project-specific paths, ensuring placeholders are replaced with actual values.
- Added a new file page for the image translation skill in the skills tab for better accessibility.
- Updated tests to verify the functionality of the new translation skill button and its integration with editable images.
This commit is contained in:
DazedAnon 2026-07-26 08:22:01 -05:00
parent 7dee0b7f20
commit 87347d5a43
6 changed files with 373 additions and 1 deletions

View file

@ -0,0 +1,266 @@
# Localize Editable Bitmap UI
<task_context>
Translate player-visible source-language text embedded in the editable RPG Maker bitmap UI
assets under this folder, recursively:
`{{EDITABLE_IMAGES_FOLDER}}`
Use this game project as read-only context for vocabulary, image usages, event data, plugins,
scripts, runtime coordinates, scaling, opacity, and dynamic values:
`{{GAME_ROOT}}`
Use this file as the authoritative glossary for every translation:
`{{VOCAB_FILE}}`
Read `vocab.txt` before translating any image. Reuse its established names, terms, spelling,
capitalization, and style consistently. If it is missing or does not cover a term, report that
gap and follow the remaining translation precedence below.
The user approves deterministic edits to the PNGs already present under the editable image
folder. Do not modify runtime game images or any other project files; the Image Manager will
patch validated edits later. Store backups and temporary candidates outside the editable image
folder so they cannot be mistaken for patchable assets. Work through all editable PNGs without
asking for confirmation unless a hard safety rule below requires review.
</task_context>
Translate embedded bitmap text by reconstructing the smallest safe UI regions and rendering
approved target text. Treat each bitmap as a structured interface asset, not merely an OCR
surface. Do not use generative image editing or inpainting.
## Required outcome
Produce localized images that:
- Preserve the original dimensions, format, color mode, and alpha channel.
- Change only approved text or panel regions.
- Leave portraits and protected artwork pixel-identical when feasible.
- Preserve empty spaces used for runtime-drawn values.
- Fit translated text without clipping or collisions.
- Retain recoverable originals.
- Include a concise validation report.
## Workflow
### 1. Establish scope
Recursively enumerate the PNGs in the editable image folder. Identify:
- Visible source-language text and intended in-place output paths.
- Authoritative glossary or vocabulary files.
- Related image variants that share a layout.
- Images without player-visible text, which must remain untouched.
The task context grants editing authority only for existing PNGs inside the editable image
folder.
### 2. Preserve originals
Create or verify a backup before changing any material asset.
- Keep the original filename and relative directory structure in the backup.
- Never overwrite the only copy.
- Reuse an existing verified backup rather than replacing it with a modified file.
- Render candidate output to a temporary path outside the editable image folder first.
### 3. Inspect every source image
Record:
- Width and height.
- Format and bit depth.
- RGB/RGBA channel layout.
- Alpha behavior.
- Visible source-language text.
- Repeated panel geometry.
- Character art and other protected regions.
Create a contact sheet when multiple variants share a layout. View each target at original
resolution before choosing coordinates.
### 4. Inspect runtime drawing logic
Search the read-only game project for every image filename and the code or event data that
displays it. Determine:
- Image position, scale, and runtime opacity.
- Runtime-drawn counters, percentages, names, or other values.
- Exact coordinates, font sizes, and alignment of dynamic text.
- Variant-selection logic.
- Whether static text is compared or read back elsewhere.
Treat runtime value locations as protected keepout regions. Do not bake values into the bitmap.
If runtime behavior cannot be established, preserve suspicious blank areas and report the
uncertainty.
### 5. Transcribe and translate
List each visible source string with its role and location. Use this precedence:
1. Authoritative project vocabulary.
2. Existing approved translations.
3. Contextual translation based on the surrounding UI.
4. A conservative literal translation when context is limited.
Preserve:
- Numbers that are already language-neutral.
- Percent signs and other functional symbols.
- Placeholder-like text such as `???`.
- Runtime value slots.
- Intentional capitalization conventions.
Do not rely on OCR alone. Verify OCR output visually, especially for stylized Japanese fonts.
### 6. Classify the background
Choose the least destructive valid removal method.
| Background | Preferred method |
| --- | --- |
| Flat UI card | Repaint the complete card |
| Rounded panel | Rebuild the rounded panel |
| Solid background | Fill the bounded text region |
| Simple gradient | Interpolate or clone a clean patch |
| Repeating texture | Clone a nearby matching region |
| Bokeh/noisy field | Clone and feather a clean patch |
| Text over artwork | Use a precise mask only when safe |
| Text crossing a character | Skip and report unless the user supplies a safe method |
Prefer whole-panel reconstruction for structured UI. It avoids blurred remnants and
source-glyph ghosts.
### 7. Define an explicit layout
For each reconstructed panel, record its bounds, corner radius, fill color, opacity model,
border, and shadow.
For each translated string, record its text, anchor or baseline, maximum permitted bounds,
font file and size, fill, outline, shadow, alignment, and minimum padding.
For each protected region, record its bounds and required policy: pixel-identical, no text, or
no overlap.
Do not improvise coordinates independently for images that share a template. Define a base
layout and apply only variant-specific overrides.
### 8. Reconstruct panels
Use sampled colors from the original panel when practical. Account for how the game displays
the asset:
- Preserve source alpha behavior when the image contains its own translucency.
- An opaque replacement card may blend correctly when the game applies runtime opacity to the
whole picture.
Replace enough of the panel to remove every source glyph. Avoid semi-transparent overlays that
leave source text visible beneath them. Do not cover frame borders, portraits, or unrelated
decoration.
### 9. Fit typography
Use real font metrics. Fit text in this order:
1. Use a font that matches the original visual weight.
2. Prefer a condensed family for narrow UI panels.
3. Measure the rendered target string.
4. Reduce font size only within a reasonable visual range.
5. Wrap only when the UI clearly permits multiple lines.
6. Shorten wording only when meaning remains accurate.
7. Skip or request review if no safe fit exists.
Use outlines or shadows consistent with the original asset. Preserve character-name colors and
other meaningful visual distinctions.
### 10. Protect dynamic values
Reserve the exact runtime-drawn areas discovered from source code or event data. Test likely
maximum values such as `0`, `99`, `100`, `999`, and any known project-specific maximum. Ensure
translated labels and suffixes cannot collide with right-aligned or centered runtime values.
### 11. Render candidates
Render from the verified original or backup, never from an earlier candidate. Prefer a
deterministic raster backend such as ImageMagick, Pillow, OpenCV, or Skia. Keep operations
explicit and repeatable.
### 12. Validate before installation
Perform all applicable checks:
- Decode the candidate successfully.
- Confirm exact expected dimensions.
- Confirm format, bit depth, and channel layout.
- Confirm alpha remains present when required.
- Compare protected crops against the original with a zero-pixel-difference target.
- Confirm changed pixels remain inside approved regions.
- Inspect every variant in a contact sheet.
- Inspect important images at original resolution.
- Simulate dynamic values at their runtime coordinates.
- Check for clipping, ghosted source glyphs, bad baselines, and collisions.
- Verify the candidate came from the original, not another modified output.
Do not install a candidate that fails a hard check.
### 13. Install atomically
After validation:
- Copy or move the candidate over its matching file in the editable image folder.
- Preserve the backup outside that folder.
- Re-run structural checks on the installed file.
- Hash the installed result when reproducibility matters.
### 14. Report concisely
Report:
- Files changed.
- Number of translated text elements.
- Representative `before -> after` examples.
- Preserved dynamic or ambiguous elements.
- Backup location.
- Validation performed.
Do not paste full image files, large encoded data, or unrelated source code.
## Hard safety rules
- Never alter a portrait or protected artwork merely to make text removal easier.
- Never bake runtime counters or percentages into a static asset.
- Never use a translucent cover that leaves readable source glyphs beneath it.
- Never assume blank space is unused when project source can be inspected.
- Never substitute a font silently when exact layout depends on it.
- Never install before reviewing the actual rendered candidate.
- Never overwrite the only original.
- Never claim pixel preservation without running a pixel comparison.
- Never modify files outside the editable image folder except for isolated backup and temporary
validation artifacts.
## Decision rules
Use whole-panel reconstruction when text is contained in a reproducible UI card, several labels
share one panel, or localized inpainting would leave artifacts.
Use localized patch replacement when the background is simple and panel reconstruction would
alter too much, or a clean neighboring texture can be cloned safely.
Skip and request review when:
- Text overlaps a character, unique illustration detail, or irregular border.
- Runtime coordinates cannot be established and collision risk is meaningful.
- Translation cannot fit without materially changing meaning.
- Alpha behavior is uncertain and the asset is composited at runtime.
## Completion criteria
Consider the task complete only when:
- All source strings in scope have a translated, preserved, skipped, or review disposition.
- Every installed bitmap passes structural validation.
- Protected regions pass their required pixel checks.
- Dynamic values have adequate space.
- Visual inspection finds no clipping or source-text ghosts.
- Originals remain recoverable.

View file

@ -14,6 +14,7 @@ from PyQt5.QtCore import (
)
from PyQt5.QtGui import QColor, QDesktopServices, QIcon, QPixmap
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QAbstractItemView,
QFileDialog,
@ -31,6 +32,7 @@ from PyQt5.QtWidgets import (
)
from util.paths import APP_NAME, ORG_NAME
from util.skills import load_clipboard_skill
from util.rpgmaker_images import (
ImageAsset,
@ -297,6 +299,12 @@ class RPGMakerImageManager(QWidget):
"editable img root."
)
self.open_workspace_button.clicked.connect(self._open_editable_folder)
self.copy_translation_button = QPushButton("Copy skill")
self.copy_translation_button.setToolTip(
"Copy an agent-ready bitmap translation skill scoped to every PNG in the editable "
"image folder. Paste it into Codex, Cursor, Copilot, or a similar coding agent."
)
self.copy_translation_button.clicked.connect(self._copy_translation_skill)
self.decrypt_selected_button = QPushButton("Decrypt")
self.decrypt_selected_button.clicked.connect(self._decrypt_checked)
self.decrypt_all_button = QPushButton("Decrypt all")
@ -319,6 +327,7 @@ class RPGMakerImageManager(QWidget):
self.prepare_button.clicked.connect(self._prepare_checked)
action_buttons = (
self.open_workspace_button,
self.copy_translation_button,
self.decrypt_selected_button,
self.decrypt_all_button,
self.remove_button,
@ -426,9 +435,9 @@ class RPGMakerImageManager(QWidget):
def _scan_done(self, generation: int, assets: list[ImageAsset]) -> None:
if generation != self._scan_generation:
return
self._set_actions_enabled(True)
self.assets = assets
self.assets_by_id = {asset.asset_id: asset for asset in assets}
self._set_actions_enabled(True)
self.selected_ids.intersection_update(self.assets_by_id)
self.folder_combo.blockSignals(True)
current_folder = self.folder_combo.currentData() or ""
@ -632,6 +641,49 @@ class RPGMakerImageManager(QWidget):
except Exception as exc:
QMessageBox.warning(self, "Editable Image Folder", str(exc))
def _editable_image_root(self) -> Path:
if self.game_root is None:
raise ValueError("Select a game folder first.")
root = Path(self.game_root).expanduser().resolve()
content_relative = resolve_content_root(root).relative_to(root)
return editable_workspace_root(root) / content_relative / "img"
def _copy_translation_skill(self) -> None:
"""Copy the bitmap-localization skill with paths for this project."""
editable_assets = self._editable_assets()
if not editable_assets:
QMessageBox.information(
self,
"No Editable Images",
"Decrypt one or more images before copying the translation skill.",
)
return
try:
if self.game_root is None:
raise ValueError("Select a game folder first.")
game_root = Path(self.game_root).expanduser().resolve()
replacements = {
"{{GAME_ROOT}}": str(game_root),
"{{EDITABLE_IMAGES_FOLDER}}": str(self._editable_image_root().resolve()),
"{{VOCAB_FILE}}": str(game_root / "vocab.txt"),
}
prompt = load_clipboard_skill("image_translation.md")
missing = [token for token in replacements if token not in prompt]
if missing:
raise ValueError(
"Image translation skill is missing required placeholder(s): "
+ ", ".join(missing)
)
for token, value in replacements.items():
prompt = prompt.replace(token, value)
QApplication.clipboard().setText(prompt)
self.status_label.setText(
f"Copied image translation skill for {len(editable_assets):,} editable PNG(s): "
f"{self._editable_image_root()}"
)
except Exception as exc:
QMessageBox.warning(self, "Copy Image Translation Skill", str(exc))
def _selected_assets(self) -> list[ImageAsset]:
return [self.assets_by_id[key] for key in self.selected_ids if key in self.assets_by_id]
@ -794,12 +846,14 @@ class RPGMakerImageManager(QWidget):
def _set_actions_enabled(self, enabled: bool) -> None:
for button in (
self.open_workspace_button,
self.copy_translation_button,
self.decrypt_selected_button,
self.decrypt_all_button,
self.remove_button,
self.prepare_button,
):
button.setEnabled(enabled)
self.copy_translation_button.setEnabled(enabled and bool(self._editable_assets()))
def closeEvent(self, event) -> None:
running = [

View file

@ -118,6 +118,17 @@ class SkillsTab(QWidget):
hint="VX Ace Ruby script localisation audit and approved in-place translation prompt.",
is_json=False,
)
self._add_file_page(
key="image_translation",
tab_title="Image TL",
path=SKILLS_DIR / "image_translation.md",
hint=(
"Clipboard skill copied from the Image Manager for deterministic bitmap UI "
"localisation. Keep the {{GAME_ROOT}}, {{EDITABLE_IMAGES_FOLDER}}, and "
"{{VOCAB_FILE}} placeholders."
),
is_json=False,
)
self._add_file_page(
key="risky_codes",
tab_title="Risky Codes",

View file

@ -26,6 +26,7 @@ _SHIPPED_DATA_FILES = (
"data/skills/wrap_config.md",
"data/skills/plugin_translation.md",
"data/skills/ace_script_translation.md",
"data/skills/image_translation.md",
"data/skills/risky_codes.md",
"data/skills/wolf_speakers.md",
"data/help/index.json",

View file

@ -236,6 +236,7 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase):
def test_bottom_controls_share_one_row_and_action_width(self):
action_buttons = (
self.manager.open_workspace_button,
self.manager.copy_translation_button,
self.manager.decrypt_selected_button,
self.manager.decrypt_all_button,
self.manager.remove_button,
@ -250,6 +251,44 @@ class RPGMakerImageManagerSelectionTests(unittest.TestCase):
self.manager.open_workspace_button.geometry().top(),
)
def test_translation_skill_is_enabled_only_for_editable_images(self):
self.assertFalse(self.manager.copy_translation_button.isEnabled())
asset = self.manager.assets[0]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
self.manager._start_scan()
self.manager._scan_worker.wait(5000)
self.app.processEvents()
self.assertTrue(self.manager.copy_translation_button.isEnabled())
def test_translation_skill_copies_project_specific_editable_folder(self):
asset = self.manager.assets[0]
asset.plain_path.parent.mkdir(parents=True, exist_ok=True)
asset.plain_path.write_bytes(asset.runtime_plain_path.read_bytes())
self.manager._start_scan()
self.manager._scan_worker.wait(5000)
self.app.processEvents()
QApplication.clipboard().clear()
self.manager.copy_translation_button.click()
prompt = QApplication.clipboard().text()
self.assertIn(str(self.game_root), prompt)
self.assertIn(str(self.game_root / ".dazedtl" / "images" / "img"), prompt)
self.assertIn(str(self.game_root / "vocab.txt"), prompt)
self.assertIn("authoritative glossary for every translation", prompt)
self.assertIn("deterministic raster backend", prompt)
self.assertIn("runtime value locations as protected keepout regions", prompt)
self.assertNotIn("{{GAME_ROOT}}", prompt)
self.assertNotIn("{{EDITABLE_IMAGES_FOLDER}}", prompt)
self.assertNotIn("{{VOCAB_FILE}}", prompt)
self.assertIn(
"Copied image translation skill for 1 editable PNG",
self.manager.status_label.text(),
)
def test_open_folder_uses_highlighted_images_editable_parent(self):
self._click(0)
asset_id = self.manager.image_list.currentItem().data(Qt.UserRole + 1)

View file

@ -40,6 +40,7 @@ class WorkflowTranslationPromptTests(unittest.TestCase):
"wrap_config.md",
"plugin_translation.md",
"ace_script_translation.md",
"image_translation.md",
"risky_codes.md",
"wolf_speakers.md",
)