i cant code anymore so let ai do it
This commit is contained in:
parent
5fd6e82b53
commit
ccdbbf6e62
8 changed files with 745 additions and 149 deletions
121
gameupdate/.gitignore
vendored
Normal file
121
gameupdate/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Ignore all files
|
||||
*.*
|
||||
|
||||
# File Types
|
||||
!*.mps
|
||||
!*.json
|
||||
!*.txt
|
||||
!*.project
|
||||
!*.js
|
||||
!*.7z
|
||||
!*.csv
|
||||
!*.ain
|
||||
!*.fnl
|
||||
!*.ks
|
||||
!*.tjs
|
||||
!*.yaml
|
||||
!*.rb
|
||||
!*.rvdata2
|
||||
|
||||
# Other Needed Files
|
||||
!.gitignore
|
||||
!README.md
|
||||
!patch-config.txt
|
||||
!GameUpdate*
|
||||
!patch*
|
||||
!Game.dat
|
||||
!bsxx.dat*
|
||||
!game.ini
|
||||
!package.nw
|
||||
!SRPG_Unpacker.exe
|
||||
!(SRPG_Unpacker Patcher).bat
|
||||
|
||||
# Ignore
|
||||
previous_patch_sha.txt
|
||||
kabe3_save.dat
|
||||
kabe3_system.dat
|
||||
psbpack.dat
|
||||
Save*
|
||||
cg.dat
|
||||
scene.dat
|
||||
BSXScript_*
|
||||
|
||||
# Images
|
||||
!Layout - コピー (2).png_
|
||||
!battleUI_sperm.png_
|
||||
!QTE_avoid.png_
|
||||
!QTE_avoid2.png_
|
||||
!QTE_miss.png_
|
||||
!QTE_miss2.png_
|
||||
!election.png_
|
||||
!name.png_
|
||||
!preg_def.png_
|
||||
!seiyoku.png_
|
||||
!father001_tinpira.png_
|
||||
!father002_tinpiraboss.png_
|
||||
!father003_goblin.png_
|
||||
!father004_ork.png_
|
||||
!father005_charao.png_
|
||||
!father006_tenta.png_
|
||||
!father007_name.png_
|
||||
!father008_student.png_
|
||||
!father009_wolf.png_
|
||||
!father010_tanemaki.png_
|
||||
!father011_dog.png_
|
||||
!father012_mayor.png_
|
||||
!father013_forceman.png_
|
||||
!father014_unknown.png_
|
||||
!father015_father.png_
|
||||
!father016_saimin.png_
|
||||
!runa_parents.png_
|
||||
!logo2.png_
|
||||
!save_window.png_
|
||||
!title1.png_
|
||||
!title2.png_
|
||||
!title3.png_
|
||||
!title3_1.png_
|
||||
!title4.png_
|
||||
!title4_1.png_
|
||||
!title4_2.png_
|
||||
!title5.png_
|
||||
!title5_1.png_
|
||||
!trial_end.png_
|
||||
!tutorial_01.png_
|
||||
!tutorial_02.png_
|
||||
!tutorial_02_00.png_
|
||||
!tutorial_02_01.png_
|
||||
!tutorial_02_02.png_
|
||||
!tutorial_02_03.png_
|
||||
!tutorial_03.png_
|
||||
!tutorial_04.png_
|
||||
!tutorial_05.png_
|
||||
!tutorial_06.png_
|
||||
!tutorial_07.png_
|
||||
!tutorial_08.png_
|
||||
!tutorial_09.png_
|
||||
!tutorial_09_01.png_
|
||||
!tutorial_10_00.png_
|
||||
!tutorial_10_01.png_
|
||||
!tutorial_11_00.png_
|
||||
!tutorial_11_01.png_
|
||||
!tutorial_12_01.png_
|
||||
!tutorial_12_02.png_
|
||||
!tutorial_12_03.png_
|
||||
!tutorial_13_00.png_
|
||||
!tutorial_13_01.png_
|
||||
!tutorial_13_02.png_
|
||||
!tutorial_14_01.png_
|
||||
!tutorial_14_02.png_
|
||||
!tutorial_21_01.png_
|
||||
!tutorial_21_02.png_
|
||||
!tutorial_21_03.png_
|
||||
!bar_hentai.png_
|
||||
!bar_org.png_
|
||||
!bar_preg.png_
|
||||
!bar_sex.png_
|
||||
!start.png_
|
||||
!title_01.png_
|
||||
!title_02.png_
|
||||
!Command_0.png_
|
||||
!Command_1.png_
|
||||
!Command_2.png_
|
||||
3
gameupdate/gameupdate/patch-config.txt
Normal file
3
gameupdate/gameupdate/patch-config.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
username=dazed-translations
|
||||
repo=magical-girls-runa-and-nanami
|
||||
branch=main
|
||||
212
gameupdate/gameupdate/vocab.txt
Normal file
212
gameupdate/gameupdate/vocab.txt
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# MAIN CHARACTERS
|
||||
エルミナ (Elmina) - Female; protagonist; elf mage
|
||||
フェリス (Feris) - Female; spirit entity; Elmina's guardian & companion
|
||||
|
||||
# ALLIES / COMPANIONS
|
||||
アンナ (Anna) - Female; witch; lives in 魔女の森; ally
|
||||
メープル (Maple) - Female; companion; timid
|
||||
ダイヤ (Daiya) - Female; companion; diligent
|
||||
トルテ (Torte) - Female; companion; cook/baker
|
||||
クレア (Claire) - Female; full name クレアリス・デカート (Clairis Decart); guild-affiliated
|
||||
ルイン (Ruin) - Female; Elmina's younger sister (妹); key story motivation
|
||||
|
||||
# SIDE / RECURRING CHARACTERS
|
||||
バブンスキー (Babunsky) - Male; antagonistic early on, later redeems himself
|
||||
ドンフシ (Donfushi) - Male; バブンスキー's younger brother; from プライア村
|
||||
ツギカ (Tsugika) - Male; young villager; has feelings for Elmina
|
||||
ミゲル (Miguel) - Male; minor NPC; innkeeper or medical role
|
||||
|
||||
# ANTAGONISTS
|
||||
アナゼマ (Anazema) - Spirit entity; primary antagonist; former co-guardian of 魔界
|
||||
ロザリー (Rosalie) - Female; ruler of カロン; arc boss
|
||||
キングディブロ (King Diabro) - Boss; Dwarf dungeon; defeated for 水の精霊石
|
||||
プーペ (Pupe) - Female; spider-type monster girl (クモ型魔物娘); recurring mini-boss
|
||||
魔神 (Demon God) - Entity; final-area antagonist; gave power to アナゼマ
|
||||
ビャクダン (Byakudan) - Named boss; Slum dungeon
|
||||
# Note: 魔神 = "Demon God" do NOT render as 'Majin'
|
||||
|
||||
# ENEMY / MONSTER TYPES
|
||||
スライム (Slime)
|
||||
オーク (Orc)
|
||||
ゴブリン (Goblin)
|
||||
ゴブリン兄弟 (Goblin Brothers)
|
||||
こうもり (Bat)
|
||||
ミノタウロス (Minotaur)
|
||||
魔物娘 (Monster Girl)
|
||||
クモ型魔物娘 (Spider-type Monster Girl) プーペ's species
|
||||
|
||||
# LOCATIONS
|
||||
## STARTING AREA
|
||||
プライア村 (Puraia Village) - Elmina's starting village
|
||||
ノストゥスの森 (Nostus Forest) - Forest east of Puraia Village; early dungeon
|
||||
|
||||
## KARON CITY
|
||||
カロン (Karon) - Major city; mid-game hub; ruled by ロザリー様
|
||||
カロン街道 (Karon Road) - Road connecting Puraia Village to Karon
|
||||
オプス (Opus) - Karon's high-end inn
|
||||
|
||||
## WITCH'S FOREST
|
||||
魔女の森 (Witch's Forest) - Anna's domain
|
||||
魔女道 (Witch's Road) - Path leading to 魔女の森
|
||||
|
||||
## DUNGEONS
|
||||
オーク洞窟 (Orc Cave) - Multi-floor orc dungeon
|
||||
ロザリー屋敷 (Rosalie's Mansion) - ロザリー's residence; 14+ floors
|
||||
|
||||
## DWARF AREA
|
||||
ドワーフ街道 (Dwarf Road) - Road from Karon toward Dwarf territory
|
||||
ドワーフ隠れ里 (Dwarf Hidden Village) - Dwarf settlement
|
||||
ドワーフダンジョン (Dwarf Dungeon) - キングディブロ boss dungeon
|
||||
|
||||
## SLUMS
|
||||
スラム (Slums) - Poverty district off the Dwarf Road
|
||||
娼館 (Brothel / Pleasure House) - Located in the Slums
|
||||
|
||||
## ELF VILLAGE
|
||||
エルフ里 (Elf Village) - Elmina's homeland; destroyed before the story begins
|
||||
|
||||
## SPIRIT STONE DUNGEONS
|
||||
精霊石ダンジョン (Spirit Stone Dungeon) - Entrance to three elemental paths
|
||||
火 (Fire) - Fire dungeon path
|
||||
風 (Wind) - Wind dungeon path
|
||||
水 (Water) - Water dungeon path
|
||||
|
||||
## DEMON WORLD
|
||||
魔界 (Demon World) - The demonic realm; final area
|
||||
# Note: render as "Demon World" NOT "Netherworld" or "Makai"
|
||||
|
||||
# WORLD-BUILDING TERMS
|
||||
淫紋 (Ero Brand / Lewd Mark) - Contract seal on Elmina's belly; converts desire into magic
|
||||
精霊石 (Spirit Stone) - Three elemental key items needed to grant Elmina's wish
|
||||
炎の精霊石 (Fire Spirit Stone)
|
||||
水の精霊石 (Water Spirit Stone)
|
||||
風の精霊石 (Wind Spirit Stone)
|
||||
魔力 (Mana / Magical Power)
|
||||
ノロイ (Curse) - アナゼマ's seal placed on the Spirit Stones
|
||||
冒険者 (Adventurer) - profession/rank system
|
||||
ギルド (Guild) - the Adventurers' Guild
|
||||
ギルド長 (Guild Master)
|
||||
調合師 (Alchemist / Apothecary) - crafts 回復ビン from ingredients
|
||||
チャームリンク (Charm Link) - magical bond item between Elmina and a companion
|
||||
協会 (Temple / Shrine) - religious institution enshrining the 女神達
|
||||
女神 (Goddess) - deities who once saved the continent
|
||||
女神達 (Goddesses) - plural; enshrined in the 協会
|
||||
魔女 (Witch) - female magic practitioner; distinct from 魔法使い (mage)
|
||||
ドワーフ族 (Dwarf Tribe / Dwarf Clan)
|
||||
エルフ (Elf) - Elmina's race
|
||||
アニキ (Aniki) - "big bro"; ドンフシ's term for バブンスキー
|
||||
|
||||
# WEAPON TYPES (Elmina's magic)
|
||||
炎の魔法 (Fire Magic) - status ailments; sustained damage
|
||||
水の魔法 (Water Magic) - single-hit damage; raises HP and Defense
|
||||
風の魔法 (Wind Magic) - multi-hit combos; gains debuffs on growth
|
||||
盾の魔法 (Shield Magic) - raises Defense; beginner-recommended
|
||||
|
||||
# ARMORS / OUTFITS
|
||||
エルミナの普段着 (Elmina's Everyday Clothes)
|
||||
絹の肌着 (Silk Undershirt)
|
||||
魅惑のバニー (Alluring Bunny)
|
||||
メイド服 (Maid Outfit)
|
||||
ワンダーウルフ (Wonder Wolf)
|
||||
調教服 (Training Outfit / Leash Outfit)
|
||||
|
||||
# BATTLE STATES
|
||||
戦闘不能 (Incapacitated / KO)
|
||||
防御 (Guard / Defending)
|
||||
不死身 (Immortal)
|
||||
毒 (Poison)
|
||||
暗闇 (Blind / Darkness)
|
||||
沈黙 (Silence)
|
||||
激昂 (Enrage / Fury)
|
||||
混乱 (Confusion)
|
||||
魅了 (Charm / Enthralled)
|
||||
睡眠 (Sleep)
|
||||
|
||||
# KEY ITEMS
|
||||
怪しげな薬 (Suspicious Medicine)
|
||||
冒険者登録証 (Adventurer's Registration Card)
|
||||
トルテのお弁当 (Torte's Lunchbox)
|
||||
回復ビン (Recovery Vial) - upgrades +1 through +15
|
||||
屋敷のカギ (Mansion Key)
|
||||
力のグローブ (Power Gloves)
|
||||
爆音袋 (Sonic Bag / Bang Bag)
|
||||
子猫の入ったカバン (Bag Containing a Kitten)
|
||||
チャームリンク・メープル (Charm Link: Maple) - +Defense
|
||||
チャームリンク・アンナ (Charm Link: Anna) - +Magic
|
||||
チャームリンク・ダイヤ (Charm Link: Daiya) - +Defense
|
||||
|
||||
## Crafting Ingredients (素材)
|
||||
緑草 (Green Grass)
|
||||
黄草 (Yellow Grass)
|
||||
赤草 (Red Grass)
|
||||
黒草 (Black Grass)
|
||||
ロゼの花 (Roze Flower)
|
||||
オルラーヤ (Orlaya)
|
||||
リンドウ (Gentian)
|
||||
ベニダケ (Red Mushroom)
|
||||
サボテンの果実 (Cactus Fruit)
|
||||
輝くゼンマイ (Glowing Bracken)
|
||||
触手の若芽 (Tentacle Sprout)
|
||||
水 (Water)
|
||||
純水 (Pure Water)
|
||||
魔力水 (Mana Water)
|
||||
|
||||
## Guild Exchange Drops (ギルド換金)
|
||||
スライムの核 (Slime Core)
|
||||
スライムの巨大核 (Slime Giant Core)
|
||||
スライムの赤核 (Slime Red Core)
|
||||
スライムの赤粘膜 (Slime Red Membrane)
|
||||
オークの牙 (Orc Fang)
|
||||
獣の毛 (Beast Fur)
|
||||
焔心核 (Flame Heart Core)
|
||||
蜘蛛の糸 (Spider Thread)
|
||||
悪魔の角 (Demon Horn)
|
||||
海魔の墨 (Kraken Ink)
|
||||
悪魔の羽根 (Demon Feather)
|
||||
死霊の灰 (Wraith Ash)
|
||||
|
||||
## Mana Crystal EXP Items
|
||||
小さな魔力結晶 (Small Mana Crystal) - EXP 10
|
||||
魔力結晶 (Mana Crystal) - EXP 100
|
||||
大きな魔力結晶 (Large Mana Crystal) - EXP 1,000
|
||||
巨大な魔力結晶 (Giant Mana Crystal) - EXP 10,000
|
||||
|
||||
## Boss Mana Cores (素材)
|
||||
プーペの魔力核 (Pupe's Mana Core)
|
||||
ビャクダンの魔力核 (Byakudan's Mana Core)
|
||||
キングディブロの魔力核 (King Diabro's Mana Core)
|
||||
ロザリーの魔力核 (Rosalie's Mana Core)
|
||||
タイタンワームの魔力結晶 (Titan Worm Mana Crystal)
|
||||
|
||||
# ROMANISATION CONVENTIONS
|
||||
エルミナ (Elmina) - not Erumina; matches title
|
||||
フェリス (Feris) - matches sprite file "feris1"
|
||||
アンナ (Anna)
|
||||
メープル (Maple)
|
||||
ダイヤ (Daiya) - or Dia; avoid "Diamond" in dialogue
|
||||
バブンスキー (Babunsky)
|
||||
ドンフシ (Donfushi)
|
||||
ツギカ (Tsugika)
|
||||
トルテ (Torte) - German: layer-cake
|
||||
クレア (Claire)
|
||||
クレアリス・デカート (Clairis Decart) - both parts are proper nouns
|
||||
アナゼマ (Anazema)
|
||||
ルイン (Ruin) - or Luin
|
||||
プーペ (Pupe) - or Poupée; French: doll
|
||||
ミゲル (Miguel)
|
||||
ロザリー (Rosalie)
|
||||
キングディブロ (King Diabro)
|
||||
ビャクダン (Byakudan)
|
||||
カロン (Karon)
|
||||
プライア村 (Puraia Village) - keep "Puraia" per map file names
|
||||
ノストゥスの森 (Nostus Forest)
|
||||
ドワーフ (Dwarf)
|
||||
エルフ (Elf)
|
||||
魔界 (Demon World) - NOT "Netherworld"; keep distinct from 地獄 "Hell"
|
||||
淫紋 (Ero Brand) - or "Lewd Mark"; keep consistent within project
|
||||
精霊石 (Spirit Stone)
|
||||
ノロイ (Curse)
|
||||
魔神 (Demon God) - NOT "Majin"; translate fully
|
||||
調合師 (Alchemist) - or Apothecary; be consistent
|
||||
ギルド長 (Guild Master)
|
||||
アニキ (Aniki) - or "Big Bro"; keep consistent per character
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
"""
|
||||
"""
|
||||
RPGMaker Workflow Tab - Automation hub for the full translation pipeline.
|
||||
|
||||
Provides a guided, step-by-step interface:
|
||||
|
||||
Step 0 – Select game project folder and import data files into files/
|
||||
Step 1 – Edit vocab.txt (glossary) inline
|
||||
Step 2 – Actor variable substitution (\\n[X] ↔ names)
|
||||
Step 3 – Auto-detect speaker format and apply to module settings
|
||||
Step 4 – Phase 1 (safe dialogue codes) and Phase 2 (risky codes) translation
|
||||
Step 5 – Export translated/ back to the game folder
|
||||
Step 1 – (Optional) Pre-process game files
|
||||
Step 2 – Auto-detect speaker format and apply to module settings
|
||||
Step 3 – Build glossary: parse speakers, then enrich with AI prompt
|
||||
Step 4 – Actor variable substitution (\\n[X] ↔ names)
|
||||
Step 5 – Phase 1 (safe dialogue codes) and Phase 2 (risky codes) translation
|
||||
Step 6 – Export translated/ back to the game folder
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -417,12 +418,12 @@ class WorkflowTab(QWidget):
|
|||
vbox.addWidget(_make_hr())
|
||||
self._build_preprocess(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step3(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step1(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step2(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step3(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step4(vbox)
|
||||
vbox.addWidget(_make_hr())
|
||||
self._build_step6(vbox)
|
||||
|
|
@ -543,29 +544,54 @@ class WorkflowTab(QWidget):
|
|||
_PROMPT_GLOSSARY = (
|
||||
"You are helping me build a complete translation glossary for a Japanese RPGMaker game.\n"
|
||||
"\n"
|
||||
"Please scan ALL of these game files (Actors.json, CommonEvents.json, Map*.json, "
|
||||
"Troops.json, and any other available files) and produce TWO sections:\n"
|
||||
"⚠️ FILE SIZE WARNING: Map files and CommonEvents.json can be extremely large "
|
||||
"(hundreds of thousands of lines). Do NOT attempt to read them in full — you will "
|
||||
"hit context limits and miss content. Instead, use this strategy:\n"
|
||||
"\n"
|
||||
" 1. Read the small structured DB files IN FULL first — these are the richest "
|
||||
"source of names and are always small:\n"
|
||||
" Actors.json, Classes.json, Troops.json, Skills.json, Items.json, "
|
||||
"Armors.json, Weapons.json, States.json, System.json\n"
|
||||
"\n"
|
||||
" 2. For large files (CommonEvents.json, Map*.json), do NOT read sequentially. "
|
||||
"Instead, SEARCH (grep) for:\n"
|
||||
" - Named character patterns: 【, \\\\n<, \\\\k< at the start of dialogue lines\n"
|
||||
" - Unique proper nouns: capitalised katakana clusters or kanji compound nouns "
|
||||
"in \"message\" or \"parameters\" fields\n"
|
||||
" Scan Map001.json through Map010.json at most — early maps have the most "
|
||||
"story-critical dialogue.\n"
|
||||
"\n"
|
||||
" 3. Stop adding entries once you stop finding new names/terms — do not pad.\n"
|
||||
"\n"
|
||||
"Produce TWO sections of output.\n"
|
||||
"\n"
|
||||
"Output the results EXACTLY in the format shown below, including the category headers "
|
||||
"starting with #. Do not add any other text, preamble, or explanation outside the entries.\n"
|
||||
"\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"SECTION 1 — NAMED CHARACTERS\n"
|
||||
"# Game Characters\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Identify every NAMED CHARACTER that appears in dialogue or descriptions.\n"
|
||||
"For each character provide:\n"
|
||||
" - Japanese name (katakana/kanji as it appears in-game)\n"
|
||||
" - English transliteration or translation\n"
|
||||
" - Gender (Male / Female / Unknown) — infer from speech patterns or pronouns\n"
|
||||
" - Brief role note (e.g. protagonist, antagonist, NPC, merchant)\n"
|
||||
" - Role (protagonist, antagonist, NPC, etc.)\n"
|
||||
" - Speech register and personality notes — how they speak, their tone, any nicknames, "
|
||||
"whether their name is player-chosen, etc.\n"
|
||||
"\n"
|
||||
"Format — one entry per line:\n"
|
||||
"山田太郎 (Yamada Taro) - Male; protagonist; high-school student\n"
|
||||
"浅井花子 (Asai Hanako) - Female; childhood friend; lives in 栄町\n"
|
||||
"Format — the header line, then one entry per line:\n"
|
||||
"# Game Characters\n"
|
||||
"\u30b7\u30ed (Shiro) - Female; protagonist; player-controlled (Actors.json ID 1); "
|
||||
"speaks in a flustered, cute register with feminine speech markers; nickname \u30d0\u30ab\u732b\u3002\n"
|
||||
"\u30af\u30ed\u30cd (Kurone) - Female; antagonist; cold and terse; speaks in short cutting sentences\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
" - Named characters only — no generic enemy types or unnamed NPCs.\n"
|
||||
" - If a character has a player-chosen name (e.g. Actors.json id 1), note it explicitly.\n"
|
||||
"\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"SECTION 2 — WORLDBUILDING TERMS\n"
|
||||
"# Worldbuilding Terms\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Identify lore-specific terms that appear in dialogue, descriptions, or narration "
|
||||
"but do NOT have a dedicated database file — i.e. terms the translation tool will NOT "
|
||||
|
|
@ -573,24 +599,53 @@ class WorkflowTab(QWidget):
|
|||
"\n"
|
||||
"Target specifically:\n"
|
||||
" - Faction / organisation names (kingdoms, guilds, cults, nations)\n"
|
||||
" - Location names that are mentioned in dialogue but are not map titles\n"
|
||||
" - Location names mentioned in dialogue but not map titles\n"
|
||||
" - Unique magic systems, schools of magic, or power classifications\n"
|
||||
" - Lore titles and honorifics unique to this setting\n"
|
||||
" - Recurring in-universe concepts, events, or proper nouns with no English equivalent\n"
|
||||
" - Recurring in-universe concepts or proper nouns with no English equivalent\n"
|
||||
"\n"
|
||||
"For each term provide:\n"
|
||||
" - Japanese original (as it appears in-game)\n"
|
||||
" - Recommended English translation\n"
|
||||
" - Brief note explaining what it is\n"
|
||||
"\n"
|
||||
"Format — one entry per line:\n"
|
||||
"魔世界 (Demon World) - The demonic realm referenced in NPC dialogue; not a named map\n"
|
||||
"聖剣教団 (Holy Blade Order) - Antagonist faction controlling the eastern territories\n"
|
||||
"Format — the header line, then one entry per line:\n"
|
||||
"# Worldbuilding Terms\n"
|
||||
"\u9b54\u4e16\u754c (Demon World) - The demonic realm referenced in NPC dialogue; not a named map\n"
|
||||
"\u8056\u5263\u6559\u56e3 (Holy Blade Order) - Antagonist faction controlling the eastern territories\n"
|
||||
"\n"
|
||||
"Rules:\n"
|
||||
" - Do NOT list skill names, item names, weapon names, armour names — the tool handles those.\n"
|
||||
" - Skip generic RPG words (ポーション, レベル, ステータス, etc.).\n"
|
||||
" - Do NOT repeat character names here — those belong in Section 1."
|
||||
" - Skip generic RPG words (\u30dd\u30fc\u30b7\u30e7\u30f3, \u30ec\u30d9\u30eb, \u30b9\u30c6\u30fc\u30bf\u30b9, etc.).\n"
|
||||
" - Do NOT repeat character names here.\n"
|
||||
"\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"COMPLETE EXAMPLE OUTPUT\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"Below is what a correct, well-formed response looks like.\n"
|
||||
"Your output should follow this structure exactly:\n"
|
||||
"\n"
|
||||
"```\n"
|
||||
"# Game Characters\n"
|
||||
"アリア (Aria) - Female; protagonist; player-chosen name (Actors.json ID 1); "
|
||||
"speaks cheerfully in casual feminine speech; nicknamed アリアちゃん by her sister\n"
|
||||
"ゼクス (Zex) - Male; antagonist; cold and commanding; addresses others with contempt; "
|
||||
"uses archaic formal register\n"
|
||||
"カナエ (Kanae) - Female; NPC shopkeeper; warm and motherly; ends sentences with わね\n"
|
||||
"\n"
|
||||
"# Worldbuilding Terms\n"
|
||||
"虚無の穴 (Void Rift) - Dimensional tear referenced repeatedly in Act 2 NPC dialogue; "
|
||||
"not a named map location\n"
|
||||
"鋼の誓約 (Iron Vow) - Sacred oath-binding ritual unique to the knightly order; "
|
||||
"appears in story cutscenes\n"
|
||||
"裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no "
|
||||
"real-world equivalent\n"
|
||||
"```\n"
|
||||
"\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
"GLOBAL RULES (apply to both sections)\n"
|
||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||
" - NEVER give two options for any term (e.g. 'Sylfia / Sylphia' is wrong). "
|
||||
"Always commit to a single best translation. If multiple transliterations exist, "
|
||||
"pick the most etymologically accurate or natural-sounding one and use only that.\n"
|
||||
" - Use a plain hyphen-minus (-) as the separator between the Japanese entry and "
|
||||
"its description. Never use an em dash (\u2014) or en dash (\u2013) \u2014 the "
|
||||
"translation tool only recognises the plain hyphen."
|
||||
)
|
||||
|
||||
# ── Step 1 (Optional): Pre-process ────────────────────────────────
|
||||
|
|
@ -693,8 +748,8 @@ class WorkflowTab(QWidget):
|
|||
tc_inner = QVBoxLayout(tc)
|
||||
tc_inner.setSpacing(4)
|
||||
tc_desc = QLabel(
|
||||
"Copies everything from the <code>gameupdate/</code> folder in the game root "
|
||||
"into the game\'s data folder, overwriting existing files."
|
||||
"Copies everything from the <code>gameupdate/</code> folder "
|
||||
"into the game\'s root folder, overwriting existing files."
|
||||
)
|
||||
tc_desc.setTextFormat(Qt.RichText)
|
||||
tc_desc.setWordWrap(True)
|
||||
|
|
@ -719,7 +774,7 @@ class WorkflowTab(QWidget):
|
|||
|
||||
tc_dst_row = QHBoxLayout()
|
||||
tc_dst_row.addWidget(QLabel("Destination:"))
|
||||
self.pp_gameupdate_dst_label = QLabel("(data folder auto-filled from project)")
|
||||
self.pp_gameupdate_dst_label = QLabel("(game root folder auto-filled from project)")
|
||||
self.pp_gameupdate_dst_label.setStyleSheet("color:#888;font-size:10px;")
|
||||
tc_dst_row.addWidget(self.pp_gameupdate_dst_label, 1)
|
||||
tc_inner.addLayout(tc_dst_row)
|
||||
|
|
@ -752,7 +807,7 @@ class WorkflowTab(QWidget):
|
|||
)
|
||||
|
||||
def _build_step1(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 2 — Glossary (vocab.txt)"))
|
||||
layout.addWidget(_make_section_label("Step 3 — Glossary (vocab.txt)"))
|
||||
hint = QLabel(
|
||||
"Edit your glossary below. Character names, genders, and worldbuilding terms "
|
||||
"here are injected into every AI prompt for consistent terminology."
|
||||
|
|
@ -761,8 +816,43 @@ class WorkflowTab(QWidget):
|
|||
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
|
||||
layout.addWidget(hint)
|
||||
|
||||
# ---- Parse Speakers -------------------------------------------------
|
||||
spk_box = QGroupBox("2a — Parse Speakers (Auto-detect Names)")
|
||||
spk_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
"margin-top:8px;font-size:11px;}"
|
||||
"QGroupBox::title{padding:0 6px;}"
|
||||
)
|
||||
spk_inner = QVBoxLayout(spk_box)
|
||||
spk_inner.setSpacing(6)
|
||||
|
||||
spk_hint = QLabel(
|
||||
"Scans every file in <b>files/</b> and extracts all detected speaker names "
|
||||
"into a <b># Speakers</b> section of vocab.txt. "
|
||||
"Run this first — the AI prompt below will then work from the pre-populated "
|
||||
"list and can enrich entries with gender, role, and speech register."
|
||||
)
|
||||
spk_hint.setTextFormat(Qt.RichText)
|
||||
spk_hint.setWordWrap(True)
|
||||
spk_hint.setStyleSheet("color:#888;font-size:10px;")
|
||||
spk_inner.addWidget(spk_hint)
|
||||
|
||||
spk_row = QHBoxLayout()
|
||||
self._parse_speakers_status = QLabel("")
|
||||
self._parse_speakers_status.setStyleSheet("color:#aaa;font-size:10px;")
|
||||
spk_row.addWidget(self._parse_speakers_status, 1)
|
||||
self._parse_speakers_btn = _make_btn("🔍 Parse Speakers", "#007acc")
|
||||
self._parse_speakers_btn.setToolTip(
|
||||
"Scan all game files for speaker names and write them to vocab.txt"
|
||||
)
|
||||
self._parse_speakers_btn.clicked.connect(self._run_parse_speakers)
|
||||
spk_row.addWidget(self._parse_speakers_btn)
|
||||
spk_inner.addLayout(spk_row)
|
||||
|
||||
layout.addWidget(spk_box)
|
||||
|
||||
# ---- Copilot / Cursor prompt helpers --------------------------------
|
||||
prompt_box = QGroupBox("AI Prompt Helpers (Copilot / Cursor)")
|
||||
prompt_box = QGroupBox("2b — AI Prompt Helpers (Copilot / Cursor)")
|
||||
prompt_box.setStyleSheet(
|
||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||
"margin-top:8px;font-size:11px;}"
|
||||
|
|
@ -772,8 +862,10 @@ class WorkflowTab(QWidget):
|
|||
pb_inner.setSpacing(6)
|
||||
|
||||
prompt_hint = QLabel(
|
||||
"Copy a prompt below, paste it into GitHub Copilot Chat or Cursor with your "
|
||||
"game files open, then paste the AI’s output into vocab.txt."
|
||||
"After parsing speakers above, copy the prompt below and paste it into "
|
||||
"GitHub Copilot Chat or Cursor with your game files open. "
|
||||
"The AI will enrich the speaker list with gender, role, and speech register "
|
||||
"and add worldbuilding terms. Paste the AI's output back into vocab.txt."
|
||||
)
|
||||
prompt_hint.setWordWrap(True)
|
||||
prompt_hint.setStyleSheet("color:#888;font-size:10px;")
|
||||
|
|
@ -800,8 +892,10 @@ class WorkflowTab(QWidget):
|
|||
# ---- vocab.txt editor -----------------------------------------------
|
||||
layout.addWidget(_make_section_label("vocab.txt editor"))
|
||||
format_hint = QLabel(
|
||||
"Expected line format: Japanese (English) - Gender; role; notes\n"
|
||||
"Example: 山田太郎 (Yamada Taro) - Male; protagonist; village blacksmith"
|
||||
"Put character entries under # Game Characters — they are always sent to the AI.\n"
|
||||
"Format: Japanese (English) - Gender; role; speech register / personality notes\n"
|
||||
"Example: シロ (Shiro) - Female; protagonist; speaks in a flustered, cute register with feminine speech markers\n"
|
||||
"Universal terms (honorifics, elements, etc.) live in vocab_base.txt and are auto-appended on every save."
|
||||
)
|
||||
format_hint.setFont(QFont("Consolas", 9))
|
||||
format_hint.setStyleSheet(
|
||||
|
|
@ -834,7 +928,7 @@ class WorkflowTab(QWidget):
|
|||
# ── Step 2: Actor Variables ─────────────────────────────────────────────
|
||||
|
||||
def _build_step2(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 3 — Actor Variable Substitution"))
|
||||
layout.addWidget(_make_section_label("Step 4 — Actor Variable Substitution"))
|
||||
hint = QLabel(
|
||||
"Replaces \\n[X] RPGMaker name variables with actual actor names "
|
||||
"so the AI has proper context. After translation the variables are "
|
||||
|
|
@ -872,7 +966,7 @@ class WorkflowTab(QWidget):
|
|||
# ── Step 3: Speaker Detection ───────────────────────────────────────────
|
||||
|
||||
def _build_step3(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 4 — Speaker Format Detection"))
|
||||
layout.addWidget(_make_section_label("Step 2 — Speaker Format Detection"))
|
||||
hint = QLabel(
|
||||
"Scans map files to determine how speaker names are embedded in dialogue "
|
||||
"and automatically sets the correct INLINE401SPEAKERS / FIRSTLINESPEAKERS "
|
||||
|
|
@ -1099,11 +1193,69 @@ class WorkflowTab(QWidget):
|
|||
# Step 1 – Vocab
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _run_parse_speakers(self):
|
||||
"""Launch a speaker-parse pass over files/ and write results to vocab.txt."""
|
||||
try:
|
||||
from gui.translation_tab import TranslationWorker
|
||||
except Exception as exc:
|
||||
self._log(f"Could not import TranslationWorker: {exc}")
|
||||
return
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
files_dir = project_root / "files"
|
||||
all_json = sorted(
|
||||
p.name for p in files_dir.glob("*.json") if p.name != ".gitkeep"
|
||||
) if files_dir.exists() else []
|
||||
|
||||
if not all_json:
|
||||
self._log("No JSON files found in files/. Run Step 1 (import) first.")
|
||||
self._parse_speakers_status.setText("No files found in files/.")
|
||||
return
|
||||
|
||||
self._log(f"Parsing speakers from {len(all_json)} file(s)...")
|
||||
self._parse_speakers_btn.setEnabled(False)
|
||||
self._parse_speakers_status.setText("Running...")
|
||||
|
||||
module_info = ["RPG Maker MV/MZ", [".json"], None]
|
||||
worker = TranslationWorker(
|
||||
project_root, module_info, estimate_only=False,
|
||||
selected_files=all_json,
|
||||
parse_speakers=True,
|
||||
)
|
||||
worker.log_signal.connect(self._log)
|
||||
worker.progress_signal.connect(
|
||||
lambda cur, tot, fn: self._parse_speakers_status.setText(
|
||||
f"[{cur}/{tot}] {Path(fn).name}"
|
||||
)
|
||||
)
|
||||
worker.finished_signal.connect(self._on_parse_speakers_done)
|
||||
self._worker = worker
|
||||
worker.start()
|
||||
|
||||
def _on_parse_speakers_done(self, ok: bool, msg: str):
|
||||
self._parse_speakers_btn.setEnabled(True)
|
||||
if ok:
|
||||
self._parse_speakers_status.setText(
|
||||
"Done — # Speakers section written to vocab.txt"
|
||||
)
|
||||
self._reload_vocab()
|
||||
self._log("Speaker parse complete. vocab.txt updated with # Speakers.")
|
||||
else:
|
||||
self._parse_speakers_status.setText(f"Failed: {msg}")
|
||||
self._log(f"Speaker parse failed: {msg}")
|
||||
|
||||
_BASE_SEPARATOR = "# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n"
|
||||
|
||||
def _reload_vocab(self):
|
||||
vocab_path = Path("vocab.txt")
|
||||
try:
|
||||
if vocab_path.exists():
|
||||
self.vocab_editor.setPlainText(vocab_path.read_text(encoding="utf-8"))
|
||||
text = vocab_path.read_text(encoding="utf-8")
|
||||
# Strip the auto-appended base section so editor shows only game-specific content
|
||||
sep_idx = text.find(self._BASE_SEPARATOR)
|
||||
if sep_idx != -1:
|
||||
text = text[:sep_idx].rstrip("\n")
|
||||
self.vocab_editor.setPlainText(text)
|
||||
else:
|
||||
self.vocab_editor.setPlainText("# Add character glossary entries here\n")
|
||||
except Exception as exc:
|
||||
|
|
@ -1111,10 +1263,12 @@ class WorkflowTab(QWidget):
|
|||
|
||||
def _save_vocab(self):
|
||||
try:
|
||||
Path("vocab.txt").write_text(
|
||||
self.vocab_editor.toPlainText(), encoding="utf-8"
|
||||
)
|
||||
self._log("✅ vocab.txt saved.")
|
||||
game_text = self.vocab_editor.toPlainText().rstrip("\n")
|
||||
base_path = Path("vocab_base.txt")
|
||||
base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
||||
combined = game_text + "\n\n" + self._BASE_SEPARATOR + base_text
|
||||
Path("vocab.txt").write_text(combined, encoding="utf-8")
|
||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||
|
||||
|
|
@ -1442,7 +1596,7 @@ class WorkflowTab(QWidget):
|
|||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.pp_gameupdate_dst_label.setText(data_path or "(no data folder detected)")
|
||||
self.pp_gameupdate_dst_label.setText(game_root or "(no game folder detected)")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
|
@ -1488,12 +1642,12 @@ class WorkflowTab(QWidget):
|
|||
|
||||
def _run_gameupdate(self):
|
||||
src = self.pp_gameupdate_edit.text().strip()
|
||||
dst = self._data_path
|
||||
dst = self.folder_edit.text().strip()
|
||||
if not src:
|
||||
self._log("⚠ No gameupdate folder path set.")
|
||||
return
|
||||
if not dst:
|
||||
self._log("⚠ No data folder detected. Complete Step 0 first.")
|
||||
self._log("⚠ No game root folder set. Complete Step 0 first.")
|
||||
return
|
||||
if not Path(src).is_dir():
|
||||
self._log(f"⚠ gameupdate folder not found: {src}")
|
||||
|
|
@ -1534,14 +1688,15 @@ class WorkflowTab(QWidget):
|
|||
skipped.append(f"B (format plugins.js): {reason}")
|
||||
|
||||
# C — gameupdate copy
|
||||
if gameupdate_src and Path(gameupdate_src).is_dir() and data_path:
|
||||
game_root_dst = self.folder_edit.text().strip()
|
||||
if gameupdate_src and Path(gameupdate_src).is_dir() and game_root_dst:
|
||||
self._log("▶ [C] gameupdate copy …")
|
||||
self._run_gameupdate()
|
||||
else:
|
||||
if not gameupdate_src or not Path(gameupdate_src).is_dir():
|
||||
reason = f"source not found ({gameupdate_src or 'not set'})"
|
||||
else:
|
||||
reason = "data folder missing"
|
||||
reason = "game root folder missing"
|
||||
skipped.append(f"C (gameupdate): {reason}")
|
||||
|
||||
for msg in skipped:
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ LEAVE = False
|
|||
# FIRSTLINESPEAKERS: Guess speaker from first line.
|
||||
FIRSTLINESPEAKERS = False
|
||||
# INLINE401SPEAKERS: Extract speaker from "Name「dialogue」" inline format on 401 lines.
|
||||
INLINE401SPEAKERS = True
|
||||
INLINE401SPEAKERS = False
|
||||
# FACENAME101: Map face name -> speaker.
|
||||
FACENAME101 = False
|
||||
# Face name -> speaker mapping for FACENAME101.
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ You will be translating erotic and sexual content. You will receive lines of dia
|
|||
|
||||
## Characters and Pronouns
|
||||
|
||||
- The **"# Game Characters"** section lists character names, nicknames, and genders. Use it to resolve any ambiguity.
|
||||
- The **"# Game Characters"** section contains character entries. Each entry may include the character's name, nicknames, gender, role, personality, and speech register notes. Read every entry carefully and apply all of it.
|
||||
- **Name & spelling:** Always use the English name given in the entry. Never invent a different romanisation.
|
||||
- **Gender:** Use the stated gender when resolving pronouns and コイツ / あいつ / こいつ references.
|
||||
- **Speech register:** If the entry describes how a character speaks (flustered, blunt, formal, childlike, crude, etc.), mirror that register in their English dialogue. A character described as speaking in a "cute, flustered register" should sound different from one described as "cold and terse".
|
||||
- **Role & context:** Use the role/personality notes to inform tone — a villain's lines should feel threatening, a comic-relief NPC's lines should feel goofy, etc.
|
||||
- Japanese omits pronouns constantly. Infer the correct subject and pronoun from context, translation history, and the character list.
|
||||
- Third-person pronouns (彼, 彼女, あいつ, こいつ, そいつ, コイツ) should match the known gender of the person being referenced.
|
||||
- Speech register matters: rough masculine speech (e.g., 俺, だ/だろ) → blunt or casual English. Soft feminine speech (e.g., 私, わ/のよ) → softer, more polite English.
|
||||
- Translate **コイツ** as "this bastard" (male) or "this bitch" (female) depending on the referenced character's gender.
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
"""
|
||||
Speaker Format Detector for RPGMaker MV/MZ
|
||||
|
||||
Scans map / event files and scores three detection modes:
|
||||
Mirrors the detection priority of rpgmakermvmz.py searchCodes() exactly:
|
||||
|
||||
INLINE401SPEAKERS – 401 lines contain Name「dialogue」
|
||||
FIRSTLINESPEAKERS – a short (< 40 char) 401 followed by 401/405 starting
|
||||
with 「 " ( ( * [
|
||||
FACENAME101 – a 101 code (Show Text / face cmd) immediately precedes
|
||||
a 401 block
|
||||
Pass 1 — scan 401/405 codes in order:
|
||||
1. \\n<Name> / \\k<Name> inline nametag codes (always active, no flag needed)
|
||||
2. 【Name】 alone on a 401 line (always active, no flag needed)
|
||||
3. 【Name】dialogue on same 401 line (always active, no flag needed)
|
||||
4. Name「dialogue」 inline quote -> INLINE401SPEAKERS
|
||||
5. Short 401 (<40 chars) followed by 401 whose
|
||||
text starts with 「 " ( ( * [ -> FIRSTLINESPEAKERS
|
||||
|
||||
Returns the best mode and confidence scores for each.
|
||||
Pass 2 — only if Pass 1 produced no reliable hits:
|
||||
6. 101 code param[0] is a non-empty name string -> FACENAME101
|
||||
|
||||
Returns the best mode and confidence scores.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -18,13 +23,28 @@ import json
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Regex used to detect inline quote speaker: "Name「..." at start of 401 text
|
||||
_INLINE_RE = re.compile(r"^([^\s「」。、!?…\\\n]{1,20})「")
|
||||
# ── Regexes matching rpgmakermvmz.py exactly ────────────────────────────────
|
||||
|
||||
# Characters that signal the next 401 is dialogue (first-line speaker heuristic)
|
||||
# \\n<Name> / \\k<Name> nametag codes (always active in module)
|
||||
_NAMETAG_RE = re.compile(
|
||||
r"[\\]+[kKnN][wWcCrRrEe]?[\[<](?:[\\]*\w\[\d+\])?(.*?)(?:[\\]*\w\[\d+\])?[>]"
|
||||
)
|
||||
|
||||
# 【Name】 alone on the line (with optional trailing control codes)
|
||||
_BRACKET_ALONE_RE = re.compile(
|
||||
r"^\s*【[^】]+】(?:\s*|(?:[\\]+[A-Za-z]+(?:\[(?:[^\[\]]|\[[^\]]*\])*\])+\s*)*)$"
|
||||
)
|
||||
|
||||
# 【Name】dialogue on the same line
|
||||
_BRACKET_INLINE_RE = re.compile(r"^\s*【([^】]+)】(.+)", re.DOTALL)
|
||||
|
||||
# Inline quote: Name「dialogue…
|
||||
_INLINE_QUOTE_RE = re.compile(r"^([^\s「」。、!?…\\\n]{1,20})「")
|
||||
|
||||
# Dialogue starters that follow a FIRSTLINESPEAKERS name line
|
||||
_DIALOGUE_STARTERS = ("「", '"', "(", "(", "*", "[")
|
||||
|
||||
# Minimum number of qualifying events to consider a result reliable
|
||||
# Minimum hits to trust a result
|
||||
_MIN_HITS = 3
|
||||
|
||||
|
||||
|
|
@ -32,29 +52,34 @@ def detect_speaker_format(
|
|||
files_dir: str | Path = "files",
|
||||
sample_size: int = 20,
|
||||
) -> dict:
|
||||
"""Scan up to *sample_size* map files and score speaker detection modes.
|
||||
"""Scan up to *sample_size* map files and determine the speaker format.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"best_mode": "INLINE401SPEAKERS" | "FIRSTLINESPEAKERS" | "FACENAME101" | "NONE",
|
||||
"best_mode": "INLINE401SPEAKERS" | "FIRSTLINESPEAKERS" | "FACENAME101"
|
||||
| "ALWAYS_ON" | "NONE",
|
||||
"scores": {
|
||||
"INLINE401SPEAKERS": int, # raw hit count
|
||||
"nametag_codes": int, # \\n<Name> hits — always handled, no flag
|
||||
"bracket_401": int, # 【Name】 hits — always handled, no flag
|
||||
"INLINE401SPEAKERS": int,
|
||||
"FIRSTLINESPEAKERS": int,
|
||||
"FACENAME101": int,
|
||||
},
|
||||
"total_401_groups": int, # total dialogue groups examined
|
||||
"total_401_groups": int,
|
||||
"files_scanned": int,
|
||||
"recommended_config": { # ready to apply to rpgmakermvmz.py
|
||||
"FIRSTLINESPEAKERS": bool,
|
||||
"recommended_config": {
|
||||
"INLINE401SPEAKERS": bool,
|
||||
"FIRSTLINESPEAKERS": bool,
|
||||
"FACENAME101": bool,
|
||||
},
|
||||
"confidence": "high" | "medium" | "low",
|
||||
"note": str, # human-readable explanation
|
||||
"note": str,
|
||||
}
|
||||
"""
|
||||
files_dir = Path(files_dir)
|
||||
scores = {
|
||||
"nametag_codes": 0,
|
||||
"bracket_401": 0,
|
||||
"INLINE401SPEAKERS": 0,
|
||||
"FIRSTLINESPEAKERS": 0,
|
||||
"FACENAME101": 0,
|
||||
|
|
@ -62,11 +87,11 @@ def detect_speaker_format(
|
|||
total_groups = 0
|
||||
files_scanned = 0
|
||||
|
||||
# Collect map files (Maps only, not MapInfos)
|
||||
# Collect map files (Maps only, not MapInfos), largest first for better signal
|
||||
map_files = sorted(
|
||||
[p for p in files_dir.glob("Map[0-9]*.json") if p.is_file()],
|
||||
key=lambda p: p.stat().st_size,
|
||||
reverse=True, # scan larger maps first for better signal
|
||||
reverse=True,
|
||||
)[:sample_size]
|
||||
|
||||
for fp in map_files:
|
||||
|
|
@ -104,46 +129,69 @@ def detect_speaker_format(
|
|||
for entry in ce_data or []:
|
||||
if not entry:
|
||||
continue
|
||||
cmd_list = entry.get("list") or []
|
||||
_score_command_list(cmd_list, scores)
|
||||
_score_command_list(entry.get("list") or [], scores)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Determine best mode
|
||||
best = max(scores, key=lambda k: scores[k])
|
||||
best_score = scores[best]
|
||||
second_best = sorted(scores.values(), reverse=True)[1]
|
||||
# ── Decision: 401-based patterns first, FACENAME101 only as fallback ────
|
||||
always_on_hits = scores["nametag_codes"] + scores["bracket_401"]
|
||||
inline_hits = scores["INLINE401SPEAKERS"]
|
||||
first_hits = scores["FIRSTLINESPEAKERS"]
|
||||
face_hits = scores["FACENAME101"]
|
||||
total_401_hits = always_on_hits + inline_hits + first_hits
|
||||
|
||||
if best_score < _MIN_HITS:
|
||||
recommended = {
|
||||
"INLINE401SPEAKERS": False,
|
||||
"FIRSTLINESPEAKERS": False,
|
||||
"FACENAME101": False,
|
||||
}
|
||||
|
||||
if total_401_hits >= _MIN_HITS:
|
||||
if always_on_hits >= _MIN_HITS and always_on_hits >= inline_hits and always_on_hits >= first_hits:
|
||||
best_mode = "ALWAYS_ON"
|
||||
confidence = "high" if always_on_hits > (inline_hits + first_hits) else "medium"
|
||||
note = (
|
||||
f"Speakers detected via \\\\n<Name> codes or 【Name】 brackets "
|
||||
f"({always_on_hits} hits) — no extra flags needed. "
|
||||
f"INLINE={inline_hits}, FIRSTLINE={first_hits}. "
|
||||
f"Scanned {files_scanned} file(s), {total_groups} dialogue group(s)."
|
||||
)
|
||||
elif inline_hits >= first_hits:
|
||||
best_mode = "INLINE401SPEAKERS"
|
||||
recommended["INLINE401SPEAKERS"] = True
|
||||
confidence = "high" if inline_hits > first_hits * 2 else "medium"
|
||||
note = (
|
||||
f"INLINE401SPEAKERS: {inline_hits} hits "
|
||||
f"(FIRSTLINESPEAKERS: {first_hits}, always-on: {always_on_hits}). "
|
||||
f"Scanned {files_scanned} file(s), {total_groups} dialogue group(s)."
|
||||
)
|
||||
else:
|
||||
best_mode = "FIRSTLINESPEAKERS"
|
||||
recommended["FIRSTLINESPEAKERS"] = True
|
||||
confidence = "high" if first_hits > inline_hits * 2 else "medium"
|
||||
note = (
|
||||
f"FIRSTLINESPEAKERS: {first_hits} hits "
|
||||
f"(INLINE401SPEAKERS: {inline_hits}, always-on: {always_on_hits}). "
|
||||
f"Scanned {files_scanned} file(s), {total_groups} dialogue group(s)."
|
||||
)
|
||||
elif face_hits >= _MIN_HITS:
|
||||
best_mode = "FACENAME101"
|
||||
recommended["FACENAME101"] = True
|
||||
confidence = "high" if face_hits > _MIN_HITS * 2 else "medium"
|
||||
note = (
|
||||
f"No 401-based speaker pattern found (401 hits: {total_401_hits}). "
|
||||
f"FACENAME101 recommended based on {face_hits} 101-code name hits. "
|
||||
f"Scanned {files_scanned} file(s), {total_groups} dialogue group(s)."
|
||||
)
|
||||
else:
|
||||
best_mode = "NONE"
|
||||
confidence = "low"
|
||||
note = (
|
||||
f"No strong speaker pattern detected "
|
||||
f"(scanned {files_scanned} file(s), {total_groups} dialogue group(s)). "
|
||||
"Consider checking speaker settings manually."
|
||||
f"No reliable speaker pattern detected "
|
||||
f"(401 hits: {total_401_hits}, FACENAME101 hits: {face_hits}). "
|
||||
f"Scanned {files_scanned} file(s), {total_groups} dialogue group(s). "
|
||||
"Check speaker settings manually."
|
||||
)
|
||||
elif best_score > second_best * 2:
|
||||
best_mode = best
|
||||
confidence = "high"
|
||||
note = (
|
||||
f"Strong signal for {best} "
|
||||
f"({best_score} hits vs {second_best} for next best, "
|
||||
f"{total_groups} dialogue group(s) in {files_scanned} file(s))."
|
||||
)
|
||||
else:
|
||||
best_mode = best
|
||||
confidence = "medium"
|
||||
note = (
|
||||
f"Moderate signal for {best} "
|
||||
f"({best_score} hits, {second_best} for next-best mode). "
|
||||
"Review a sample of dialogue manually to confirm."
|
||||
)
|
||||
|
||||
recommended = {
|
||||
"FIRSTLINESPEAKERS": best_mode == "FIRSTLINESPEAKERS",
|
||||
"INLINE401SPEAKERS": best_mode == "INLINE401SPEAKERS",
|
||||
"FACENAME101": best_mode == "FACENAME101",
|
||||
}
|
||||
|
||||
return {
|
||||
"best_mode": best_mode,
|
||||
|
|
@ -156,69 +204,80 @@ def detect_speaker_format(
|
|||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
# ── Internal helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
def _score_command_list(cmd_list: list, scores: dict) -> None:
|
||||
"""Walk a command list and increment scores for detected patterns."""
|
||||
"""Walk a command list and score speaker patterns in module priority order."""
|
||||
if not cmd_list:
|
||||
return
|
||||
|
||||
last_101_face: str | None = None # face name from most recent 101 cmd
|
||||
|
||||
i = 0
|
||||
while i < len(cmd_list):
|
||||
cmd = cmd_list[i]
|
||||
code = cmd.get("code")
|
||||
params = cmd.get("parameters") or []
|
||||
|
||||
# ---- 101: Show Text (face) ----
|
||||
# ── 101: Show Text / face name ────────────────────────────────────────
|
||||
if code == 101:
|
||||
# params[0] = face name, params[1] = face index
|
||||
last_101_face = (params[0] if params else None) or ""
|
||||
# FACENAME101: param[0] is a non-empty face-name string
|
||||
face = params[0] if params else ""
|
||||
if isinstance(face, str) and face.strip():
|
||||
scores["FACENAME101"] += 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# ---- 401 / 405: dialogue line ----
|
||||
# ── 401 / 405: dialogue line ──────────────────────────────────────────
|
||||
if code in (401, 405):
|
||||
text = (params[0] if params else "") or ""
|
||||
|
||||
# Score INLINE401SPEAKERS
|
||||
if _INLINE_RE.match(text):
|
||||
# 1. \\n<Name> nametag codes (always active — no flag needed)
|
||||
if _NAMETAG_RE.search(text):
|
||||
scores["nametag_codes"] += 1
|
||||
i += 1
|
||||
continue # module strips the nametag before further processing
|
||||
|
||||
# 2+3. 【Name】 alone on line, or 【Name】dialogue inline (always active)
|
||||
if _BRACKET_ALONE_RE.match(text) or _BRACKET_INLINE_RE.match(text):
|
||||
scores["bracket_401"] += 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# 4. INLINE401SPEAKERS: Name「dialogue
|
||||
if _INLINE_QUOTE_RE.match(text):
|
||||
scores["INLINE401SPEAKERS"] += 1
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Score FACENAME101 — if face was set right before this block
|
||||
if last_101_face is not None:
|
||||
scores["FACENAME101"] += 1
|
||||
last_101_face = None # only count once per block
|
||||
|
||||
# Score FIRSTLINESPEAKERS — short line with no dialogue starters,
|
||||
# followed by a 401/405 that starts with a dialogue starter
|
||||
# 5. FIRSTLINESPEAKERS: short line followed by 401 starting with
|
||||
# a dialogue-starter character
|
||||
if (
|
||||
len(text) < 40
|
||||
and not any(text.lstrip().startswith(s) for s in _DIALOGUE_STARTERS)
|
||||
and _has_japanese(text)
|
||||
and not any(text.lstrip().startswith(s) for s in _DIALOGUE_STARTERS)
|
||||
):
|
||||
j = i + 1
|
||||
while j < len(cmd_list) and cmd_list[j].get("code") == -1:
|
||||
j += 1
|
||||
if j < len(cmd_list) and cmd_list[j].get("code") in (401, 405):
|
||||
next_text = ((cmd_list[j].get("parameters") or [""])[0]) or ""
|
||||
if next_text.lstrip().startswith(_DIALOGUE_STARTERS):
|
||||
next_params = cmd_list[j].get("parameters") or []
|
||||
next_text = (next_params[0] if next_params else "") or ""
|
||||
# Strip leading RPGMaker control codes before checking starter
|
||||
next_stripped = re.sub(
|
||||
r"^(?:[\\]+[^cCnNiIkKvVSs{}]+?\[[\d\w\W]+?\]?\])+",
|
||||
"", next_text,
|
||||
).lstrip()
|
||||
if next_stripped and next_stripped[0] in _DIALOGUE_STARTERS:
|
||||
scores["FIRSTLINESPEAKERS"] += 1
|
||||
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Any non-dialogue code resets the face context
|
||||
if code not in (-1,):
|
||||
last_101_face = None
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
_JP_RE = re.compile(r"[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uF900-\uFAFF\uFF61-\uFF9F]")
|
||||
_JP_RE = re.compile(
|
||||
r"[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uF900-\uFAFF\uFF61-\uFF9F]"
|
||||
)
|
||||
|
||||
|
||||
def _has_japanese(text: str) -> bool:
|
||||
|
|
|
|||
|
|
@ -674,6 +674,9 @@ def buildMatchedVocabText(vocabPairs, subbedText, history=None):
|
|||
return matchedVocabText
|
||||
|
||||
|
||||
_BASE_VOCAB_SEPARATOR = "# ── Base Vocabulary" # Prefix of the separator line written by _save_vocab
|
||||
|
||||
|
||||
def createContext(config, fullPromptFlag, subbedText, formatType, history=None):
|
||||
"""Create system and user messages for translation.
|
||||
|
||||
|
|
@ -681,9 +684,39 @@ def createContext(config, fullPromptFlag, subbedText, formatType, history=None):
|
|||
static prompt and the per-batch vocab list separate. This lets Claude
|
||||
prompt-caching mark only the stable prefix with cache_control, avoiding
|
||||
cache invalidation caused by changing vocabulary matches.
|
||||
|
||||
Cached in static_system (never changes between batches):
|
||||
- prompt.txt content
|
||||
- # Game Characters entries
|
||||
- Base vocabulary (vocab_base.txt: honorifics, elements, demons, etc.)
|
||||
|
||||
Dynamic in vocab_text (matched per-batch):
|
||||
- All other game-specific vocab terms (items, worldbuilding, etc.)
|
||||
"""
|
||||
vocabPairs = parseVocabWithCategories(config.vocab)
|
||||
matchedVocabText = buildMatchedVocabText(vocabPairs, subbedText, history)
|
||||
vocab_full = config.vocab
|
||||
|
||||
# Split game-specific vocab from the static base vocab appended by _save_vocab
|
||||
base_vocab_text = ""
|
||||
sep_idx = vocab_full.find(_BASE_VOCAB_SEPARATOR)
|
||||
if sep_idx != -1:
|
||||
# Strip the separator header line itself; keep the sections underneath
|
||||
base_section = vocab_full[sep_idx:]
|
||||
# Skip the separator line
|
||||
newline_idx = base_section.find("\n")
|
||||
base_vocab_text = base_section[newline_idx + 1:].strip() if newline_idx != -1 else ""
|
||||
vocab_game = vocab_full[:sep_idx]
|
||||
else:
|
||||
vocab_game = vocab_full
|
||||
|
||||
vocabPairs = parseVocabWithCategories(vocab_game)
|
||||
|
||||
# Split character entries (stable, cached) from everything else (dynamic)
|
||||
CHAR_CAT = "# Game Characters"
|
||||
char_lines = [line for _, line, cat in vocabPairs if cat and cat.strip() == CHAR_CAT]
|
||||
non_char_pairs = [(term, line, cat) for term, line, cat in vocabPairs
|
||||
if not (cat and cat.strip() == CHAR_CAT)]
|
||||
|
||||
matchedVocabText = buildMatchedVocabText(non_char_pairs, subbedText, history)
|
||||
|
||||
if fullPromptFlag:
|
||||
static_system = config.prompt.replace("English", config.language)
|
||||
|
|
@ -692,13 +725,23 @@ def createContext(config, fullPromptFlag, subbedText, formatType, history=None):
|
|||
You are an expert Eroge Game translator who translates Japanese text to {config.language}.
|
||||
Output ONLY the {config.language} translation in the following format: `Translation: <{config.language.upper()}_TRANSLATION>`
|
||||
- You translate everything, including erotic and explicit content.
|
||||
- 'Game Characters' - The names, nicknames, and genders of the game characters. Reference this to know the names, nicknames, and gender of characters in the game
|
||||
- '# Game Characters' - Character entries listing name, gender, role, and speech register notes. Use ALL fields: apply the correct English name, resolve gendered pronouns, and match each character's stated speech register and personality in their dialogue.
|
||||
- All text in your response must be in {config.language} even if it is hard to translate.
|
||||
- Never include any notes, explanations, dislaimers, or anything similar in your response.
|
||||
- Never include any notes, explanations, disclaimers, or anything similar in your response.
|
||||
- Maintain any spacing in the translation.
|
||||
- `...` can be a part of the dialogue. Translate it as it is.
|
||||
"""
|
||||
|
||||
# Append character glossary to the stable system prompt so it is cached
|
||||
if char_lines:
|
||||
char_block = f"\n{CHAR_CAT}\n" + "\n".join(char_lines)
|
||||
static_system = static_system.rstrip() + "\n" + char_block + "\n"
|
||||
|
||||
# Append base vocab (honorifics, elements, etc.) to static_system so it is
|
||||
# cached with the system prompt and never re-evaluated per batch.
|
||||
if base_vocab_text:
|
||||
static_system = static_system.rstrip() + "\n\n" + base_vocab_text + "\n"
|
||||
|
||||
if formatType == "json":
|
||||
user = f"```json\n{subbedText}\n```"
|
||||
else:
|
||||
|
|
|
|||
Loading…
Reference in a new issue