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.
|
RPGMaker Workflow Tab - Automation hub for the full translation pipeline.
|
||||||
|
|
||||||
Provides a guided, step-by-step interface:
|
Provides a guided, step-by-step interface:
|
||||||
|
|
||||||
Step 0 – Select game project folder and import data files into files/
|
Step 0 – Select game project folder and import data files into files/
|
||||||
Step 1 – Edit vocab.txt (glossary) inline
|
Step 1 – (Optional) Pre-process game files
|
||||||
Step 2 – Actor variable substitution (\\n[X] ↔ names)
|
Step 2 – Auto-detect speaker format and apply to module settings
|
||||||
Step 3 – Auto-detect speaker format and apply to module settings
|
Step 3 – Build glossary: parse speakers, then enrich with AI prompt
|
||||||
Step 4 – Phase 1 (safe dialogue codes) and Phase 2 (risky codes) translation
|
Step 4 – Actor variable substitution (\\n[X] ↔ names)
|
||||||
Step 5 – Export translated/ back to the game folder
|
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
|
from __future__ import annotations
|
||||||
|
|
@ -417,12 +418,12 @@ class WorkflowTab(QWidget):
|
||||||
vbox.addWidget(_make_hr())
|
vbox.addWidget(_make_hr())
|
||||||
self._build_preprocess(vbox)
|
self._build_preprocess(vbox)
|
||||||
vbox.addWidget(_make_hr())
|
vbox.addWidget(_make_hr())
|
||||||
|
self._build_step3(vbox)
|
||||||
|
vbox.addWidget(_make_hr())
|
||||||
self._build_step1(vbox)
|
self._build_step1(vbox)
|
||||||
vbox.addWidget(_make_hr())
|
vbox.addWidget(_make_hr())
|
||||||
self._build_step2(vbox)
|
self._build_step2(vbox)
|
||||||
vbox.addWidget(_make_hr())
|
vbox.addWidget(_make_hr())
|
||||||
self._build_step3(vbox)
|
|
||||||
vbox.addWidget(_make_hr())
|
|
||||||
self._build_step4(vbox)
|
self._build_step4(vbox)
|
||||||
vbox.addWidget(_make_hr())
|
vbox.addWidget(_make_hr())
|
||||||
self._build_step6(vbox)
|
self._build_step6(vbox)
|
||||||
|
|
@ -543,29 +544,54 @@ class WorkflowTab(QWidget):
|
||||||
_PROMPT_GLOSSARY = (
|
_PROMPT_GLOSSARY = (
|
||||||
"You are helping me build a complete translation glossary for a Japanese RPGMaker game.\n"
|
"You are helping me build a complete translation glossary for a Japanese RPGMaker game.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Please scan ALL of these game files (Actors.json, CommonEvents.json, Map*.json, "
|
"⚠️ FILE SIZE WARNING: Map files and CommonEvents.json can be extremely large "
|
||||||
"Troops.json, and any other available files) and produce TWO sections:\n"
|
"(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"
|
||||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||||
"SECTION 1 — NAMED CHARACTERS\n"
|
"# Game Characters\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||||
"Identify every NAMED CHARACTER that appears in dialogue or descriptions.\n"
|
"Identify every NAMED CHARACTER that appears in dialogue or descriptions.\n"
|
||||||
"For each character provide:\n"
|
"For each character provide:\n"
|
||||||
" - Japanese name (katakana/kanji as it appears in-game)\n"
|
" - Japanese name (katakana/kanji as it appears in-game)\n"
|
||||||
" - English transliteration or translation\n"
|
" - English transliteration or translation\n"
|
||||||
" - Gender (Male / Female / Unknown) — infer from speech patterns or pronouns\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"
|
"\n"
|
||||||
"Format — one entry per line:\n"
|
"Format — the header line, then one entry per line:\n"
|
||||||
"山田太郎 (Yamada Taro) - Male; protagonist; high-school student\n"
|
"# Game Characters\n"
|
||||||
"浅井花子 (Asai Hanako) - Female; childhood friend; lives in 栄町\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"
|
"\n"
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
" - Named characters only — no generic enemy types or unnamed NPCs.\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"
|
" - If a character has a player-chosen name (e.g. Actors.json id 1), note it explicitly.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||||
"SECTION 2 — WORLDBUILDING TERMS\n"
|
"# Worldbuilding Terms\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
||||||
"Identify lore-specific terms that appear in dialogue, descriptions, or narration "
|
"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 "
|
"but do NOT have a dedicated database file — i.e. terms the translation tool will NOT "
|
||||||
|
|
@ -573,24 +599,53 @@ class WorkflowTab(QWidget):
|
||||||
"\n"
|
"\n"
|
||||||
"Target specifically:\n"
|
"Target specifically:\n"
|
||||||
" - Faction / organisation names (kingdoms, guilds, cults, nations)\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"
|
" - Unique magic systems, schools of magic, or power classifications\n"
|
||||||
" - Lore titles and honorifics unique to this setting\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"
|
"\n"
|
||||||
"For each term provide:\n"
|
"Format — the header line, then one entry per line:\n"
|
||||||
" - Japanese original (as it appears in-game)\n"
|
"# Worldbuilding Terms\n"
|
||||||
" - Recommended English translation\n"
|
"\u9b54\u4e16\u754c (Demon World) - The demonic realm referenced in NPC dialogue; not a named map\n"
|
||||||
" - Brief note explaining what it is\n"
|
"\u8056\u5263\u6559\u56e3 (Holy Blade Order) - Antagonist faction controlling the eastern territories\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"
|
|
||||||
"\n"
|
"\n"
|
||||||
"Rules:\n"
|
"Rules:\n"
|
||||||
" - Do NOT list skill names, item names, weapon names, armour names — the tool handles those.\n"
|
" - Do NOT list skill names, item names, weapon names, armour names — the tool handles those.\n"
|
||||||
" - Skip generic RPG words (ポーション, レベル, ステータス, etc.).\n"
|
" - 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 — those belong in Section 1."
|
" - 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 ────────────────────────────────
|
# ── Step 1 (Optional): Pre-process ────────────────────────────────
|
||||||
|
|
@ -693,8 +748,8 @@ class WorkflowTab(QWidget):
|
||||||
tc_inner = QVBoxLayout(tc)
|
tc_inner = QVBoxLayout(tc)
|
||||||
tc_inner.setSpacing(4)
|
tc_inner.setSpacing(4)
|
||||||
tc_desc = QLabel(
|
tc_desc = QLabel(
|
||||||
"Copies everything from the <code>gameupdate/</code> folder in the game root "
|
"Copies everything from the <code>gameupdate/</code> folder "
|
||||||
"into the game\'s data folder, overwriting existing files."
|
"into the game\'s root folder, overwriting existing files."
|
||||||
)
|
)
|
||||||
tc_desc.setTextFormat(Qt.RichText)
|
tc_desc.setTextFormat(Qt.RichText)
|
||||||
tc_desc.setWordWrap(True)
|
tc_desc.setWordWrap(True)
|
||||||
|
|
@ -719,7 +774,7 @@ class WorkflowTab(QWidget):
|
||||||
|
|
||||||
tc_dst_row = QHBoxLayout()
|
tc_dst_row = QHBoxLayout()
|
||||||
tc_dst_row.addWidget(QLabel("Destination:"))
|
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;")
|
self.pp_gameupdate_dst_label.setStyleSheet("color:#888;font-size:10px;")
|
||||||
tc_dst_row.addWidget(self.pp_gameupdate_dst_label, 1)
|
tc_dst_row.addWidget(self.pp_gameupdate_dst_label, 1)
|
||||||
tc_inner.addLayout(tc_dst_row)
|
tc_inner.addLayout(tc_dst_row)
|
||||||
|
|
@ -752,7 +807,7 @@ class WorkflowTab(QWidget):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _build_step1(self, layout: QVBoxLayout):
|
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(
|
hint = QLabel(
|
||||||
"Edit your glossary below. Character names, genders, and worldbuilding terms "
|
"Edit your glossary below. Character names, genders, and worldbuilding terms "
|
||||||
"here are injected into every AI prompt for consistent terminology."
|
"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;")
|
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
|
||||||
layout.addWidget(hint)
|
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 --------------------------------
|
# ---- Copilot / Cursor prompt helpers --------------------------------
|
||||||
prompt_box = QGroupBox("AI Prompt Helpers (Copilot / Cursor)")
|
prompt_box = QGroupBox("2b — AI Prompt Helpers (Copilot / Cursor)")
|
||||||
prompt_box.setStyleSheet(
|
prompt_box.setStyleSheet(
|
||||||
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
"QGroupBox{color:#ccc;border:1px solid #444;border-radius:3px;"
|
||||||
"margin-top:8px;font-size:11px;}"
|
"margin-top:8px;font-size:11px;}"
|
||||||
|
|
@ -772,8 +862,10 @@ class WorkflowTab(QWidget):
|
||||||
pb_inner.setSpacing(6)
|
pb_inner.setSpacing(6)
|
||||||
|
|
||||||
prompt_hint = QLabel(
|
prompt_hint = QLabel(
|
||||||
"Copy a prompt below, paste it into GitHub Copilot Chat or Cursor with your "
|
"After parsing speakers above, copy the prompt below and paste it into "
|
||||||
"game files open, then paste the AI’s output into vocab.txt."
|
"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.setWordWrap(True)
|
||||||
prompt_hint.setStyleSheet("color:#888;font-size:10px;")
|
prompt_hint.setStyleSheet("color:#888;font-size:10px;")
|
||||||
|
|
@ -800,8 +892,10 @@ class WorkflowTab(QWidget):
|
||||||
# ---- vocab.txt editor -----------------------------------------------
|
# ---- vocab.txt editor -----------------------------------------------
|
||||||
layout.addWidget(_make_section_label("vocab.txt editor"))
|
layout.addWidget(_make_section_label("vocab.txt editor"))
|
||||||
format_hint = QLabel(
|
format_hint = QLabel(
|
||||||
"Expected line format: Japanese (English) - Gender; role; notes\n"
|
"Put character entries under # Game Characters — they are always sent to the AI.\n"
|
||||||
"Example: 山田太郎 (Yamada Taro) - Male; protagonist; village blacksmith"
|
"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.setFont(QFont("Consolas", 9))
|
||||||
format_hint.setStyleSheet(
|
format_hint.setStyleSheet(
|
||||||
|
|
@ -834,7 +928,7 @@ class WorkflowTab(QWidget):
|
||||||
# ── Step 2: Actor Variables ─────────────────────────────────────────────
|
# ── Step 2: Actor Variables ─────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_step2(self, layout: QVBoxLayout):
|
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(
|
hint = QLabel(
|
||||||
"Replaces \\n[X] RPGMaker name variables with actual actor names "
|
"Replaces \\n[X] RPGMaker name variables with actual actor names "
|
||||||
"so the AI has proper context. After translation the variables are "
|
"so the AI has proper context. After translation the variables are "
|
||||||
|
|
@ -872,7 +966,7 @@ class WorkflowTab(QWidget):
|
||||||
# ── Step 3: Speaker Detection ───────────────────────────────────────────
|
# ── Step 3: Speaker Detection ───────────────────────────────────────────
|
||||||
|
|
||||||
def _build_step3(self, layout: QVBoxLayout):
|
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(
|
hint = QLabel(
|
||||||
"Scans map files to determine how speaker names are embedded in dialogue "
|
"Scans map files to determine how speaker names are embedded in dialogue "
|
||||||
"and automatically sets the correct INLINE401SPEAKERS / FIRSTLINESPEAKERS "
|
"and automatically sets the correct INLINE401SPEAKERS / FIRSTLINESPEAKERS "
|
||||||
|
|
@ -1099,11 +1193,69 @@ class WorkflowTab(QWidget):
|
||||||
# Step 1 – Vocab
|
# 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):
|
def _reload_vocab(self):
|
||||||
vocab_path = Path("vocab.txt")
|
vocab_path = Path("vocab.txt")
|
||||||
try:
|
try:
|
||||||
if vocab_path.exists():
|
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:
|
else:
|
||||||
self.vocab_editor.setPlainText("# Add character glossary entries here\n")
|
self.vocab_editor.setPlainText("# Add character glossary entries here\n")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
@ -1111,10 +1263,12 @@ class WorkflowTab(QWidget):
|
||||||
|
|
||||||
def _save_vocab(self):
|
def _save_vocab(self):
|
||||||
try:
|
try:
|
||||||
Path("vocab.txt").write_text(
|
game_text = self.vocab_editor.toPlainText().rstrip("\n")
|
||||||
self.vocab_editor.toPlainText(), encoding="utf-8"
|
base_path = Path("vocab_base.txt")
|
||||||
)
|
base_text = base_path.read_text(encoding="utf-8") if base_path.exists() else ""
|
||||||
self._log("✅ vocab.txt saved.")
|
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:
|
except Exception as exc:
|
||||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||||
|
|
||||||
|
|
@ -1442,7 +1596,7 @@ class WorkflowTab(QWidget):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -1488,12 +1642,12 @@ class WorkflowTab(QWidget):
|
||||||
|
|
||||||
def _run_gameupdate(self):
|
def _run_gameupdate(self):
|
||||||
src = self.pp_gameupdate_edit.text().strip()
|
src = self.pp_gameupdate_edit.text().strip()
|
||||||
dst = self._data_path
|
dst = self.folder_edit.text().strip()
|
||||||
if not src:
|
if not src:
|
||||||
self._log("⚠ No gameupdate folder path set.")
|
self._log("⚠ No gameupdate folder path set.")
|
||||||
return
|
return
|
||||||
if not dst:
|
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
|
return
|
||||||
if not Path(src).is_dir():
|
if not Path(src).is_dir():
|
||||||
self._log(f"⚠ gameupdate folder not found: {src}")
|
self._log(f"⚠ gameupdate folder not found: {src}")
|
||||||
|
|
@ -1534,14 +1688,15 @@ class WorkflowTab(QWidget):
|
||||||
skipped.append(f"B (format plugins.js): {reason}")
|
skipped.append(f"B (format plugins.js): {reason}")
|
||||||
|
|
||||||
# C — gameupdate copy
|
# 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._log("▶ [C] gameupdate copy …")
|
||||||
self._run_gameupdate()
|
self._run_gameupdate()
|
||||||
else:
|
else:
|
||||||
if not gameupdate_src or not Path(gameupdate_src).is_dir():
|
if not gameupdate_src or not Path(gameupdate_src).is_dir():
|
||||||
reason = f"source not found ({gameupdate_src or 'not set'})"
|
reason = f"source not found ({gameupdate_src or 'not set'})"
|
||||||
else:
|
else:
|
||||||
reason = "data folder missing"
|
reason = "game root folder missing"
|
||||||
skipped.append(f"C (gameupdate): {reason}")
|
skipped.append(f"C (gameupdate): {reason}")
|
||||||
|
|
||||||
for msg in skipped:
|
for msg in skipped:
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ LEAVE = False
|
||||||
# FIRSTLINESPEAKERS: Guess speaker from first line.
|
# FIRSTLINESPEAKERS: Guess speaker from first line.
|
||||||
FIRSTLINESPEAKERS = False
|
FIRSTLINESPEAKERS = False
|
||||||
# INLINE401SPEAKERS: Extract speaker from "Name「dialogue」" inline format on 401 lines.
|
# INLINE401SPEAKERS: Extract speaker from "Name「dialogue」" inline format on 401 lines.
|
||||||
INLINE401SPEAKERS = True
|
INLINE401SPEAKERS = False
|
||||||
# FACENAME101: Map face name -> speaker.
|
# FACENAME101: Map face name -> speaker.
|
||||||
FACENAME101 = False
|
FACENAME101 = False
|
||||||
# Face name -> speaker mapping for FACENAME101.
|
# 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
|
## 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.
|
- 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.
|
- 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.
|
- 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
|
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」
|
Pass 1 — scan 401/405 codes in order:
|
||||||
FIRSTLINESPEAKERS – a short (< 40 char) 401 followed by 401/405 starting
|
1. \\n<Name> / \\k<Name> inline nametag codes (always active, no flag needed)
|
||||||
with 「 " ( ( * [
|
2. 【Name】 alone on a 401 line (always active, no flag needed)
|
||||||
FACENAME101 – a 101 code (Show Text / face cmd) immediately precedes
|
3. 【Name】dialogue on same 401 line (always active, no flag needed)
|
||||||
a 401 block
|
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
|
from __future__ import annotations
|
||||||
|
|
@ -18,13 +23,28 @@ import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Regex used to detect inline quote speaker: "Name「..." at start of 401 text
|
# ── Regexes matching rpgmakermvmz.py exactly ────────────────────────────────
|
||||||
_INLINE_RE = re.compile(r"^([^\s「」。、!?…\\\n]{1,20})「")
|
|
||||||
|
|
||||||
# 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 = ("「", '"', "(", "(", "*", "[")
|
_DIALOGUE_STARTERS = ("「", '"', "(", "(", "*", "[")
|
||||||
|
|
||||||
# Minimum number of qualifying events to consider a result reliable
|
# Minimum hits to trust a result
|
||||||
_MIN_HITS = 3
|
_MIN_HITS = 3
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,41 +52,46 @@ def detect_speaker_format(
|
||||||
files_dir: str | Path = "files",
|
files_dir: str | Path = "files",
|
||||||
sample_size: int = 20,
|
sample_size: int = 20,
|
||||||
) -> dict:
|
) -> 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:
|
Returns:
|
||||||
{
|
{
|
||||||
"best_mode": "INLINE401SPEAKERS" | "FIRSTLINESPEAKERS" | "FACENAME101" | "NONE",
|
"best_mode": "INLINE401SPEAKERS" | "FIRSTLINESPEAKERS" | "FACENAME101"
|
||||||
|
| "ALWAYS_ON" | "NONE",
|
||||||
"scores": {
|
"scores": {
|
||||||
"INLINE401SPEAKERS": int, # raw hit count
|
"nametag_codes": int, # \\n<Name> hits — always handled, no flag
|
||||||
"FIRSTLINESPEAKERS": int,
|
"bracket_401": int, # 【Name】 hits — always handled, no flag
|
||||||
"FACENAME101": int,
|
"INLINE401SPEAKERS": int,
|
||||||
|
"FIRSTLINESPEAKERS": int,
|
||||||
|
"FACENAME101": int,
|
||||||
},
|
},
|
||||||
"total_401_groups": int, # total dialogue groups examined
|
"total_401_groups": int,
|
||||||
"files_scanned": int,
|
"files_scanned": int,
|
||||||
"recommended_config": { # ready to apply to rpgmakermvmz.py
|
"recommended_config": {
|
||||||
"FIRSTLINESPEAKERS": bool,
|
|
||||||
"INLINE401SPEAKERS": bool,
|
"INLINE401SPEAKERS": bool,
|
||||||
|
"FIRSTLINESPEAKERS": bool,
|
||||||
"FACENAME101": bool,
|
"FACENAME101": bool,
|
||||||
},
|
},
|
||||||
"confidence": "high" | "medium" | "low",
|
"confidence": "high" | "medium" | "low",
|
||||||
"note": str, # human-readable explanation
|
"note": str,
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
files_dir = Path(files_dir)
|
files_dir = Path(files_dir)
|
||||||
scores = {
|
scores = {
|
||||||
|
"nametag_codes": 0,
|
||||||
|
"bracket_401": 0,
|
||||||
"INLINE401SPEAKERS": 0,
|
"INLINE401SPEAKERS": 0,
|
||||||
"FIRSTLINESPEAKERS": 0,
|
"FIRSTLINESPEAKERS": 0,
|
||||||
"FACENAME101": 0,
|
"FACENAME101": 0,
|
||||||
}
|
}
|
||||||
total_groups = 0
|
total_groups = 0
|
||||||
files_scanned = 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(
|
map_files = sorted(
|
||||||
[p for p in files_dir.glob("Map[0-9]*.json") if p.is_file()],
|
[p for p in files_dir.glob("Map[0-9]*.json") if p.is_file()],
|
||||||
key=lambda p: p.stat().st_size,
|
key=lambda p: p.stat().st_size,
|
||||||
reverse=True, # scan larger maps first for better signal
|
reverse=True,
|
||||||
)[:sample_size]
|
)[:sample_size]
|
||||||
|
|
||||||
for fp in map_files:
|
for fp in map_files:
|
||||||
|
|
@ -104,121 +129,155 @@ def detect_speaker_format(
|
||||||
for entry in ce_data or []:
|
for entry in ce_data or []:
|
||||||
if not entry:
|
if not entry:
|
||||||
continue
|
continue
|
||||||
cmd_list = entry.get("list") or []
|
_score_command_list(entry.get("list") or [], scores)
|
||||||
_score_command_list(cmd_list, scores)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Determine best mode
|
# ── Decision: 401-based patterns first, FACENAME101 only as fallback ────
|
||||||
best = max(scores, key=lambda k: scores[k])
|
always_on_hits = scores["nametag_codes"] + scores["bracket_401"]
|
||||||
best_score = scores[best]
|
inline_hits = scores["INLINE401SPEAKERS"]
|
||||||
second_best = sorted(scores.values(), reverse=True)[1]
|
first_hits = scores["FIRSTLINESPEAKERS"]
|
||||||
|
face_hits = scores["FACENAME101"]
|
||||||
if best_score < _MIN_HITS:
|
total_401_hits = always_on_hits + inline_hits + first_hits
|
||||||
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."
|
|
||||||
)
|
|
||||||
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 = {
|
recommended = {
|
||||||
"FIRSTLINESPEAKERS": best_mode == "FIRSTLINESPEAKERS",
|
"INLINE401SPEAKERS": False,
|
||||||
"INLINE401SPEAKERS": best_mode == "INLINE401SPEAKERS",
|
"FIRSTLINESPEAKERS": False,
|
||||||
"FACENAME101": best_mode == "FACENAME101",
|
"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 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."
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"best_mode": best_mode,
|
"best_mode": best_mode,
|
||||||
"scores": scores,
|
"scores": scores,
|
||||||
"total_401_groups": total_groups,
|
"total_401_groups": total_groups,
|
||||||
"files_scanned": files_scanned,
|
"files_scanned": files_scanned,
|
||||||
"recommended_config": recommended,
|
"recommended_config": recommended,
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
"note": note,
|
"note": note,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ── Internal helpers ─────────────────────────────────────────────────────────
|
||||||
# Internal helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _score_command_list(cmd_list: list, scores: dict) -> None:
|
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:
|
if not cmd_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
last_101_face: str | None = None # face name from most recent 101 cmd
|
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(cmd_list):
|
while i < len(cmd_list):
|
||||||
cmd = cmd_list[i]
|
cmd = cmd_list[i]
|
||||||
code = cmd.get("code")
|
code = cmd.get("code")
|
||||||
params = cmd.get("parameters") or []
|
params = cmd.get("parameters") or []
|
||||||
|
|
||||||
# ---- 101: Show Text (face) ----
|
# ── 101: Show Text / face name ────────────────────────────────────────
|
||||||
if code == 101:
|
if code == 101:
|
||||||
# params[0] = face name, params[1] = face index
|
# FACENAME101: param[0] is a non-empty face-name string
|
||||||
last_101_face = (params[0] if params else None) or ""
|
face = params[0] if params else ""
|
||||||
|
if isinstance(face, str) and face.strip():
|
||||||
|
scores["FACENAME101"] += 1
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# ---- 401 / 405: dialogue line ----
|
# ── 401 / 405: dialogue line ──────────────────────────────────────────
|
||||||
if code in (401, 405):
|
if code in (401, 405):
|
||||||
text = (params[0] if params else "") or ""
|
text = (params[0] if params else "") or ""
|
||||||
|
|
||||||
# Score INLINE401SPEAKERS
|
# 1. \\n<Name> nametag codes (always active — no flag needed)
|
||||||
if _INLINE_RE.match(text):
|
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
|
scores["INLINE401SPEAKERS"] += 1
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
# Score FACENAME101 — if face was set right before this block
|
# 5. FIRSTLINESPEAKERS: short line followed by 401 starting with
|
||||||
if last_101_face is not None:
|
# a dialogue-starter character
|
||||||
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
|
|
||||||
if (
|
if (
|
||||||
len(text) < 40
|
len(text) < 40
|
||||||
and not any(text.lstrip().startswith(s) for s in _DIALOGUE_STARTERS)
|
|
||||||
and _has_japanese(text)
|
and _has_japanese(text)
|
||||||
|
and not any(text.lstrip().startswith(s) for s in _DIALOGUE_STARTERS)
|
||||||
):
|
):
|
||||||
j = i + 1
|
j = i + 1
|
||||||
while j < len(cmd_list) and cmd_list[j].get("code") == -1:
|
while j < len(cmd_list) and cmd_list[j].get("code") == -1:
|
||||||
j += 1
|
j += 1
|
||||||
if j < len(cmd_list) and cmd_list[j].get("code") in (401, 405):
|
if j < len(cmd_list) and cmd_list[j].get("code") in (401, 405):
|
||||||
next_text = ((cmd_list[j].get("parameters") or [""])[0]) or ""
|
next_params = cmd_list[j].get("parameters") or []
|
||||||
if next_text.lstrip().startswith(_DIALOGUE_STARTERS):
|
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
|
scores["FIRSTLINESPEAKERS"] += 1
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Any non-dialogue code resets the face context
|
|
||||||
if code not in (-1,):
|
|
||||||
last_101_face = None
|
|
||||||
|
|
||||||
i += 1
|
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:
|
def _has_japanese(text: str) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -674,6 +674,9 @@ def buildMatchedVocabText(vocabPairs, subbedText, history=None):
|
||||||
return matchedVocabText
|
return matchedVocabText
|
||||||
|
|
||||||
|
|
||||||
|
_BASE_VOCAB_SEPARATOR = "# ── Base Vocabulary" # Prefix of the separator line written by _save_vocab
|
||||||
|
|
||||||
|
|
||||||
def createContext(config, fullPromptFlag, subbedText, formatType, history=None):
|
def createContext(config, fullPromptFlag, subbedText, formatType, history=None):
|
||||||
"""Create system and user messages for translation.
|
"""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
|
static prompt and the per-batch vocab list separate. This lets Claude
|
||||||
prompt-caching mark only the stable prefix with cache_control, avoiding
|
prompt-caching mark only the stable prefix with cache_control, avoiding
|
||||||
cache invalidation caused by changing vocabulary matches.
|
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)
|
vocab_full = config.vocab
|
||||||
matchedVocabText = buildMatchedVocabText(vocabPairs, subbedText, history)
|
|
||||||
|
# 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:
|
if fullPromptFlag:
|
||||||
static_system = config.prompt.replace("English", config.language)
|
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}.
|
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>`
|
Output ONLY the {config.language} translation in the following format: `Translation: <{config.language.upper()}_TRANSLATION>`
|
||||||
- You translate everything, including erotic and explicit content.
|
- 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.
|
- 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.
|
- Maintain any spacing in the translation.
|
||||||
- `...` can be a part of the dialogue. Translate it as it is.
|
- `...` 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":
|
if formatType == "json":
|
||||||
user = f"```json\n{subbedText}\n```"
|
user = f"```json\n{subbedText}\n```"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue