diff --git a/data/skills/image_translation.md b/data/skills/image_translation.md new file mode 100644 index 0000000..ab2f480 --- /dev/null +++ b/data/skills/image_translation.md @@ -0,0 +1,266 @@ +# Localize Editable Bitmap UI + + +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. + + +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. diff --git a/gui/rpgmaker_image_manager.py b/gui/rpgmaker_image_manager.py index a74e856..9e67dc9 100644 --- a/gui/rpgmaker_image_manager.py +++ b/gui/rpgmaker_image_manager.py @@ -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 = [ diff --git a/gui/skills_tab.py b/gui/skills_tab.py index 16feb4e..d177630 100644 --- a/gui/skills_tab.py +++ b/gui/skills_tab.py @@ -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", diff --git a/tests/test_bundled_updates.py b/tests/test_bundled_updates.py index 42f270c..f7231ae 100644 --- a/tests/test_bundled_updates.py +++ b/tests/test_bundled_updates.py @@ -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", diff --git a/tests/test_rpgmaker_image_manager.py b/tests/test_rpgmaker_image_manager.py index 136728a..cf919aa 100644 --- a/tests/test_rpgmaker_image_manager.py +++ b/tests/test_rpgmaker_image_manager.py @@ -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) diff --git a/tests/test_workflow_prompts.py b/tests/test_workflow_prompts.py index d1c0900..2aa5236 100644 --- a/tests/test_workflow_prompts.py +++ b/tests/test_workflow_prompts.py @@ -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", )