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:
parent
7dee0b7f20
commit
87347d5a43
6 changed files with 373 additions and 1 deletions
266
data/skills/image_translation.md
Normal file
266
data/skills/image_translation.md
Normal 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.
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue