overlay: 大ログにトランスリテレーション(ルビ)対応を追加し描画/APIを拡張

- controller/model: transliteration_message / transliteration_translation を伝搬するよう変更し、createOverlayImage* 呼び出しの引数を更新
- overlay: createTextboxLargeLogWithRubyTokens を実装し、大ログでのトークン単位ルビ描画(フォールバックロジック、外側パディング、行間等)を追加
- overlay: 小型ログAPI/呼び出しを transliteration_* 名に合わせて修正・簡素化
- docs: overlay_ruby.md に大ログ向け仕様と使用例を追記
This commit is contained in:
misyaguziya
2025-10-23 14:41:37 +09:00
parent 66d3c09a0d
commit 0a9cb9952b
4 changed files with 305 additions and 113 deletions

View File

@@ -270,7 +270,7 @@ class Controller:
elif isinstance(message, str) and len(message) > 0: elif isinstance(message, str) and len(message) > 0:
translation = [] translation = []
transliteration_message: List[Any] = [] transliteration_message = []
transliteration_translation = [] transliteration_translation = []
if model.checkKeywords(message): if model.checkKeywords(message):
self.run( self.run(
@@ -383,7 +383,9 @@ class Controller:
None, None,
None, None,
translation, translation,
config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO] config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)
else: else:
@@ -392,7 +394,9 @@ class Controller:
message, message,
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"], config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"],
translation, translation,
config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO] config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)
@@ -426,7 +430,7 @@ class Controller:
) )
elif isinstance(message, str) and len(message) > 0: elif isinstance(message, str) and len(message) > 0:
translation = [] translation = []
transliteration_message: List[Any] = [] transliteration_message = []
transliteration_translation = [] transliteration_translation = []
if model.checkKeywords(message): if model.checkKeywords(message):
self.run( self.run(
@@ -511,6 +515,8 @@ class Controller:
None, None,
translation, translation,
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO], config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlaySmallLog(overlay_image) model.updateOverlaySmallLog(overlay_image)
else: else:
@@ -519,6 +525,8 @@ class Controller:
language, language,
translation, translation,
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO], config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlaySmallLog(overlay_image) model.updateOverlaySmallLog(overlay_image)
@@ -530,6 +538,9 @@ class Controller:
None, None,
None, None,
translation, translation,
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)
else: else:
@@ -538,7 +549,9 @@ class Controller:
message, message,
language, language,
translation, translation,
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO] config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)
@@ -703,6 +716,8 @@ class Controller:
None, None,
translation, translation,
config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO], config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)
else: else:
@@ -712,6 +727,8 @@ class Controller:
config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"], config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"],
translation, translation,
config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO], config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
transliteration_message,
transliteration_translation
) )
model.updateOverlayLargeLog(overlay_image) model.updateOverlayLargeLog(overlay_image)

View File

@@ -8,6 +8,36 @@
- `model.createOverlayImageSmallLog` 内で自動的に `convertMessageToTransliteration(..., hiragana=True, romaji=True)` を呼び出しトークン生成。 - `model.createOverlayImageSmallLog` 内で自動的に `convertMessageToTransliteration(..., hiragana=True, romaji=True)` を呼び出しトークン生成。
- 生成されたトークンに `hepburn` または `hira` が含まれる。 - 生成されたトークンに `hepburn` または `hira` が含まれる。
## 大ログ (Large Log) への拡張
大ログについてもトークン単位のルビ描画をサポートしました。`createOverlayImageLargeLog` / `createTextboxLargeLog` 系の API に以下のような追加引数が入り、同等のルビ出力が可能です。
- transliteration_tokens: Optional[List[dict]] — 原文用トークンorig/hira/hepburn
- translation_transliteration_tokens: Optional[List[List[dict]] | List[dict]] — 翻訳ごとのトークン配列、もしくは先頭翻訳用の平坦な List[dict]
- ruby_font_scale: float — ルビのフォント倍率(原文フォントサイズに対するスケール)
- ruby_line_spacing: int — ルビ行間ピクセル
対応挙動:
- 原文のみが存在し、その原文にトークンがあれば原文にトークン単位ルビを振ります。
- 原文と翻訳の両方が存在する場合は、原則として原文のルビを抑止し、翻訳側にルビを振ります(翻訳側にトークンがある場合)。
- `translation_transliteration_tokens` は二通りの入力形式を受け付けます:
- List[List[dict]] — 各翻訳行ごとの tokens 配列(推奨)
- List[dict] — 平坦な tokens 配列(最初の翻訳行に適用されます)
フォールバック:
- トークン単位レイアウトで横幅がはみ出す or 改行がある場合は、既存のブロックルビromaji 上 / hira 下 を 1 行ブロックで表示)へ自動フォールバックします。
注意点:
- 既存の表示ロジックの互換性を保つため、引数は省略可能ですNone/[])。
- フラットな `translation_transliteration_tokens` を渡す場合は最初の翻訳にのみ適用されます。複数翻訳に個別のルビを渡す場合は List[List[dict]] 形式で与えてください。
## 設定キー (`config.OVERLAY_SMALL_LOG_SETTINGS`) ## 設定キー (`config.OVERLAY_SMALL_LOG_SETTINGS`)
| キー | 型 | 初期値 | 説明 | | キー | 型 | 初期値 | 説明 |
| ---- | --- | ------ | ---- | | ---- | --- | ------ | ---- |
@@ -15,15 +45,41 @@
| ruby_line_spacing | int | 4 | ローマ字行とひらがな行の垂直スペース (px)。0〜200 | | ruby_line_spacing | int | 4 | ローマ字行とひらがな行の垂直スペース (px)。0〜200 |
## レイアウト仕様 ## レイアウト仕様
1. ルビブロック (romaji 上 / hiragana 下) を中央揃えで描画。 1. ルビブロック (romaji 上 / hiragana 下) を中央揃えで描画。
2. その下に従来の本文テキストボックスを縦方向に連結。 2. その下に従来の本文テキストボックスを縦方向に連結。
3. フォントファミリは本文と同一 (言語に対応する NotoSans 系)。 3. フォントファミリは本文と同一 (言語に対応する NotoSans 系)。
4. ルビが存在しない場合は従来表示のみ。 4. ルビが存在しない場合は従来表示のみ。
## フォールバック ## フォールバック
- ルビ生成中に例外が発生した場合はログを記録し、ルビ無しで本文のみ表示。 - ルビ生成中に例外が発生した場合はログを記録し、ルビ無しで本文のみ表示。
- トークンが空の場合(両方 False など)は従来表示。 - トークンが空の場合(両方 False など)は従来表示。
## 例
以下は `createOverlayImageLargeLog` を使って、翻訳側にだけルビを渡す例(平坦な tokens を渡す場合と翻訳ごとの tokens を渡す場合):
```python
# 平坦な tokens を渡して最初の翻訳に適用
overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_tokens=[], translation_transliteration_tokens=[
{"orig": "こんにちは", "hira": "こんにちは", "hepburn": "konnichiha"},
{"orig": "世界", "hira": "せかい", "hepburn": "sekai"},
])
# 翻訳ごとに tokens を与える(推奨)
overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_tokens=[], translation_transliteration_tokens=[
[
{"orig": "Hello", "hira": "", "hepburn": "Hello"},
{"orig": "World", "hira": "", "hepburn": "World"},
]
])
```
## 今後の拡張候補 ## 今後の拡張候補
- 翻訳行へのルビ付与オプション。 - 翻訳行へのルビ付与オプション。
- トークン単位での幅センタリングと折り返し。 - トークン単位での幅センタリングと折り返し。

View File

@@ -944,38 +944,30 @@ class Model:
self.speaker_energy_recorder.stop() self.speaker_energy_recorder.stop()
self.speaker_energy_recorder = None self.speaker_energy_recorder = None
def createOverlayImageSmallLog(self, message:Optional[str], your_language:Optional[str], translation:list, target_language:Optional[dict], translation_transliteration_tokens: Optional[list] = None) -> object: def createOverlayImageSmallLog(self, message:Optional[str], your_language:Optional[str], translation:list, target_language:Optional[dict], transliteration_message:Optional[dict] = None, transliteration_translation:Optional[list] = None) -> object:
self.ensure_initialized() self.ensure_initialized()
# Normalize target_language dict -> list # Normalize target_language dict -> list
target_language_list = [] target_language_list = []
if isinstance(target_language, dict): if isinstance(target_language, dict):
target_language_list = [data["language"] for data in target_language.values() if data.get("enable") is True] target_language_list = [data["language"] for data in target_language.values() if data.get("enable") is True]
# Prepare transliteration tokens only if we have an original message string.
transliteration_tokens = []
if isinstance(message, str) and message.strip():
try:
# Always request both romaji + hiragana for ruby (per spec: romaji upper, hiragana lower)
transliteration_tokens = self.convertMessageToTransliteration(message, hiragana=True, romaji=True)
except Exception:
transliteration_tokens = []
errorLogging()
# Fetch ruby settings from config (with safe defaults if missing) # Fetch ruby settings from config (with safe defaults if missing)
ruby_font_scale = config.OVERLAY_SMALL_LOG_SETTINGS.get("ruby_font_scale", 0.5) ruby_font_scale = config.OVERLAY_SMALL_LOG_SETTINGS.get("ruby_font_scale", 0.5)
ruby_line_spacing = config.OVERLAY_SMALL_LOG_SETTINGS.get("ruby_line_spacing", 4) ruby_line_spacing = config.OVERLAY_SMALL_LOG_SETTINGS.get("ruby_line_spacing", 4)
# 翻訳行ルビ (任意) が指定されていれば渡す。後方互換のため None / 不正型は空リストに。 # 翻訳行ルビ (任意) が指定されていれば渡す。後方互換のため None / 不正型は空リストに。
if not isinstance(translation_transliteration_tokens, list): if not isinstance(transliteration_message, list):
translation_transliteration_tokens = [] transliteration_message = []
if not isinstance(transliteration_translation, list):
transliteration_translation = [[] for _ in translation]
return self.overlay_image.createOverlayImageSmallLog( return self.overlay_image.createOverlayImageSmallLog(
message, message,
your_language, your_language,
translation, translation,
target_language_list, target_language_list,
transliteration_tokens=transliteration_tokens, transliteration_message=transliteration_message,
translation_transliteration_tokens=translation_transliteration_tokens, transliteration_translation=transliteration_translation,
ruby_font_scale=ruby_font_scale, ruby_font_scale=ruby_font_scale,
ruby_line_spacing=ruby_line_spacing, ruby_line_spacing=ruby_line_spacing,
) )
@@ -1031,13 +1023,13 @@ class Model:
if (self.overlay.settings[size]["ui_scaling"] != config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"]): if (self.overlay.settings[size]["ui_scaling"] != config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"]):
self.overlay.updateUiScaling(config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"], size) self.overlay.updateUiScaling(config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"], size)
def createOverlayImageLargeLog(self, message_type:str, message:Optional[str], your_language:Optional[str], translation:list, target_language:Optional[dict]=None): def createOverlayImageLargeLog(self, message_type:str, message:Optional[str], your_language:Optional[str], translation:list, target_language:Optional[dict]=None, transliteration_message:Optional[list]=None, transliteration_translation:Optional[list]=None) -> object:
self.ensure_initialized() self.ensure_initialized()
# normalize target_language dict -> list of language strings # normalize target_language dict -> list of language strings
target_language_list = [] target_language_list = []
if isinstance(target_language, dict): if isinstance(target_language, dict):
target_language_list = [data["language"] for data in target_language.values() if data.get("enable") is True] target_language_list = [data["language"] for data in target_language.values() if data.get("enable") is True]
return self.overlay_image.createOverlayImageLargeLog(message_type, message, your_language, translation, target_language_list) return self.overlay_image.createOverlayImageLargeLog(message_type, message, your_language, translation, target_language_list, transliteration_message, transliteration_translation)
def createOverlayImageLargeMessage(self, message): def createOverlayImageLargeMessage(self, message):
self.ensure_initialized() self.ensure_initialized()

View File

@@ -106,10 +106,10 @@ class OverlayImage:
draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center") draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center")
return img return img
def renderRubyBlock(self, transliteration_tokens: List[dict], language: str, base_width: int, base_font_size: int, ruby_font_scale: float, ruby_line_spacing: int, text_color: Tuple[int, int, int]) -> Optional[Image.Image]: def renderRubyBlock(self, transliteration: List[dict], language: str, base_width: int, base_font_size: int, ruby_font_scale: float, ruby_line_spacing: int, text_color: Tuple[int, int, int]) -> Optional[Image.Image]:
# Build romaji and hiragana lines. # Build romaji and hiragana lines.
romaji_line = " ".join([t.get("hepburn", "") for t in transliteration_tokens if t.get("hepburn")]) romaji_line = " ".join([t.get("hepburn", "") for t in transliteration if t.get("hepburn")])
hira_line = " ".join([t.get("hira", "") for t in transliteration_tokens if t.get("hira")]) hira_line = " ".join([t.get("hira", "") for t in transliteration if t.get("hira")])
if not romaji_line and not hira_line: if not romaji_line and not hira_line:
return None return None
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"]) font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
@@ -136,12 +136,12 @@ class OverlayImage:
draw.text((hira_x + hira_width // 2, current_y), hira_line, text_color, anchor="mm", font=font_ruby) draw.text((hira_x + hira_width // 2, current_y), hira_line, text_color, anchor="mm", font=font_ruby)
return ruby_img return ruby_img
def createTextboxSmallLogWithRubyTokens(self, message: str, transliteration_tokens: List[dict], language: str, text_color: Tuple[int, int, int], base_width: int, font_size: int, ruby_font_scale: float, ruby_line_spacing: int, ruby_original_spacing: int) -> Image: def createTextboxSmallLogWithRubyTokens(self, message: str, transliteration: List[dict], language: str, text_color: Tuple[int, int, int], base_width: int, font_size: int, ruby_font_scale: float, ruby_line_spacing: int, ruby_original_spacing: int) -> Image:
"""Render a single textbox (original message) with per-token centered ruby (romaji above hiragana) over each original token. """Render a single textbox (original message) with per-token centered ruby (romaji above hiragana) over each original token.
Fallback: if wrapping would occur (message too wide) or tokens mismatch, revert to block-level ruby (renderRubyBlock + createTextboxSmallLog). Fallback: if wrapping would occur (message too wide) or tokens mismatch, revert to block-level ruby (renderRubyBlock + createTextboxSmallLog).
""" """
if not message or not transliteration_tokens: if not message or not transliteration:
return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size) return self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
# Obtain font instances # Obtain font instances
@@ -155,7 +155,7 @@ class OverlayImage:
draw_tmp = ImageDraw.Draw(draw_tmp_img) draw_tmp = ImageDraw.Draw(draw_tmp_img)
token_infos = [] token_infos = []
total_width = 0 total_width = 0
for tok in transliteration_tokens: for tok in transliteration:
orig = tok.get("orig", "") orig = tok.get("orig", "")
if not orig: if not orig:
continue continue
@@ -170,7 +170,7 @@ class OverlayImage:
if not token_infos: if not token_infos:
# Fallback # Fallback
ruby_block = self.renderRubyBlock(transliteration_tokens, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color) ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size) base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
if ruby_block: if ruby_block:
return self.concatenateImagesVertically(ruby_block, base_img) return self.concatenateImagesVertically(ruby_block, base_img)
@@ -178,7 +178,7 @@ class OverlayImage:
# Simple wrapping detection: if total width exceeds base_width * 0.9 → fallback # Simple wrapping detection: if total width exceeds base_width * 0.9 → fallback
if total_width > base_width * 0.9: if total_width > base_width * 0.9:
ruby_block = self.renderRubyBlock(transliteration_tokens, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color) ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size) base_img = self.createTextboxSmallLog(message, language, text_color, base_width, self.getUiSizeSmallLog()["height"], font_size)
if ruby_block: if ruby_block:
return self.concatenateImagesVertically(ruby_block, base_img) return self.concatenateImagesVertically(ruby_block, base_img)
@@ -224,7 +224,8 @@ class OverlayImage:
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig) draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
cursor_x += w cursor_x += w
return img return img
def createOverlayImageSmallLog(self, message: str, your_language: str, translation: List[str] = [], target_language: List[str] = [], transliteration_tokens: List[dict] = [], translation_transliteration_tokens: List[List[dict]] = [], ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
def createOverlayImageSmallLog(self, message: str, your_language: str, translation: List[str] = [], target_language: List[str] = [], transliteration_message: List[dict] = [], transliteration_translation: List[List[dict]] = [], ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
# UI設定を取得 # UI設定を取得
ui_size = self.getUiSizeSmallLog() ui_size = self.getUiSizeSmallLog()
width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"] width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"]
@@ -242,65 +243,32 @@ class OverlayImage:
ruby_original_spacing = 2 # Narrow vertical gap between hiragana block and original text. ruby_original_spacing = 2 # Narrow vertical gap between hiragana block and original text.
if translation and target_language: if translation and target_language:
if message: if message:
if transliteration_tokens: base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
try:
base_msg_img = self.createTextboxSmallLogWithRubyTokens(
message,
transliteration_tokens,
your_language,
text_color,
width,
font_size,
ruby_font_scale,
ruby_line_spacing,
ruby_original_spacing,
)
except Exception:
errorLogging()
# Fallback to old method
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
try:
ruby_img = self.renderRubyBlock(transliteration_tokens, your_language, width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
if ruby_img is not None:
base_msg_img = self.concatenateImagesVertically(ruby_img, base_msg_img)
except Exception:
errorLogging()
else:
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
textbox_images.append(base_msg_img) textbox_images.append(base_msg_img)
for idx, (trans, lang) in enumerate(zip(translation, target_language)): for trans, lang, translite in zip(translation, target_language, transliteration_translation):
# 翻訳行用ルビ (任意) translation_transliteration_tokens[idx] が存在すれば使用 try:
per_trans_tokens: List[dict] = [] trans_img = self.createTextboxSmallLogWithRubyTokens(
if idx < len(translation_transliteration_tokens): trans,
candidate = translation_transliteration_tokens[idx] translite,
if isinstance(candidate, list): lang,
per_trans_tokens = candidate text_color,
if per_trans_tokens: width,
try: font_size,
trans_img = self.createTextboxSmallLogWithRubyTokens( ruby_font_scale,
trans, ruby_line_spacing,
per_trans_tokens, ruby_original_spacing,
lang, )
text_color, except Exception:
width, errorLogging()
font_size,
ruby_font_scale,
ruby_line_spacing,
ruby_original_spacing,
)
except Exception:
errorLogging()
trans_img = self.createTextboxSmallLog(trans, lang, text_color, width, height, font_size)
else:
trans_img = self.createTextboxSmallLog(trans, lang, text_color, width, height, font_size) trans_img = self.createTextboxSmallLog(trans, lang, text_color, width, height, font_size)
textbox_images.append(trans_img) textbox_images.append(trans_img)
else: else:
# 翻訳無しモード # 翻訳無しモード
if message and transliteration_tokens: if message and transliteration_message:
try: try:
base_msg_img = self.createTextboxSmallLogWithRubyTokens( base_msg_img = self.createTextboxSmallLogWithRubyTokens(
message, message,
transliteration_tokens, transliteration_message,
your_language, your_language,
text_color, text_color,
width, width,
@@ -313,7 +281,7 @@ class OverlayImage:
errorLogging() errorLogging()
base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size) base_msg_img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
try: try:
ruby_img = self.renderRubyBlock(transliteration_tokens, your_language, width, font_size, ruby_font_scale, ruby_line_spacing, text_color) ruby_img = self.renderRubyBlock(transliteration_message, your_language, width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
if ruby_img is not None: if ruby_img is not None:
base_msg_img = self.concatenateImagesVertically(ruby_img, base_msg_img) base_msg_img = self.concatenateImagesVertically(ruby_img, base_msg_img)
except Exception: except Exception:
@@ -392,6 +360,93 @@ class OverlayImage:
draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align) draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align)
return img return img
def createTextboxLargeLogWithRubyTokens(self, message_type: str, size: str, message: str, transliteration: List[dict], language: str, ruby_font_scale: float, ruby_line_spacing: int) -> Image:
"""Render a large-log textbox with per-token centered ruby above each original token.
Falls back to block-level ruby if message wraps or tokens mismatch.
"""
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
text_color = self.getUiColorLargeLog()[f"text_color_{size}"]
font_family = self.LANGUAGES.get(language, self.LANGUAGES["Default"])
font_orig = self._get_font(font_family, font_size)
ruby_size = max(1, int(font_size * ruby_font_scale))
font_ruby = self._get_font(font_family, ruby_size)
# Simple guard
if not message or not transliteration:
return self.createTextImageLargeLog(message_type, size, message, language)
# Reject multiline for per-token layout; fallback to block ruby
if "\n" in message:
ruby_block = self.renderRubyBlock(transliteration, language, ui_size["width"], font_size, ruby_font_scale, ruby_line_spacing, text_color)
base_img = self.createTextImageLargeLog(message_type, size, message, language)
if ruby_block is not None:
return self.concatenateImagesVertically(ruby_block, base_img)
return base_img
# Measure token widths
draw_tmp_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
draw_tmp = ImageDraw.Draw(draw_tmp_img)
token_infos = []
total_width = 0
for tok in transliteration:
orig = tok.get("orig", "")
if not orig:
continue
hira = tok.get("hira", "")
romaji = tok.get("hepburn", "")
orig_w = max(1, int(draw_tmp.textlength(orig, font_orig)))
hira_w = max(0, int(draw_tmp.textlength(hira, font_ruby))) if hira else 0
romaji_w = max(0, int(draw_tmp.textlength(romaji, font_ruby))) if romaji else 0
layout_w = max(orig_w, hira_w, romaji_w)
token_infos.append((orig, hira, romaji, layout_w))
total_width += layout_w
# Fallback if nothing to render or would overflow
base_width = ui_size["width"]
if not token_infos or total_width > base_width * 0.9:
ruby_block = self.renderRubyBlock(transliteration, language, base_width, font_size, ruby_font_scale, ruby_line_spacing, text_color)
base_img = self.createTextImageLargeLog(message_type, size, message, language)
if ruby_block is not None:
return self.concatenateImagesVertically(ruby_block, base_img)
return base_img
# Determine start_x according to message type (left for receive, right-align for send)
start_x = 0 if message_type == "receive" else (base_width - total_width)
# Vertical layout
outer_padding = 10
has_romaji_any = any(r for (_, _, r, _) in token_infos)
has_hira_any = any(h for (_, h, _, _) in token_infos)
ruby_lines_count = (1 if has_romaji_any else 0) + (1 if has_hira_any else 0)
ruby_block_height = ruby_lines_count * ruby_size + (ruby_line_spacing if ruby_lines_count == 2 else 0)
total_height = outer_padding + ruby_block_height + font_size + outer_padding
img = Image.new("RGBA", (base_width, total_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# y positions
current_y = outer_padding + ruby_size // 2
romaji_y = current_y if has_romaji_any else None
hira_y = None
if has_romaji_any and has_hira_any:
hira_y = romaji_y + ruby_size + ruby_line_spacing
elif has_hira_any:
hira_y = current_y
orig_y = outer_padding + ruby_block_height + font_size // 2
# Draw tokens
cursor_x = start_x
for orig, hira, romaji, w in token_infos:
token_center_x = cursor_x + w // 2
if romaji_y is not None and romaji:
draw.text((token_center_x, romaji_y), romaji, text_color, anchor="mm", font=font_ruby)
if hira_y is not None and hira:
draw.text((token_center_x, hira_y), hira, text_color, anchor="mm", font=font_ruby)
draw.text((token_center_x, orig_y), orig, text_color, anchor="mm", font=font_orig)
cursor_x += w
return img
def createTextImageMessageType(self, message_type: str, date_time: str) -> Image: def createTextImageMessageType(self, message_type: str, date_time: str) -> Image:
ui_size = self.getUiSizeLargeLog() ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_small"] font_size = ui_size["font_size_small"]
@@ -422,7 +477,7 @@ class OverlayImage:
draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font) draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font)
return img return img
def createTextboxLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], date_time: Optional[str] = None) -> Image: def createTextboxLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], date_time: Optional[str] = None, transliteration_message: Optional[List[dict]] = None, transliteration_translation: Optional[List[List[dict]]] = None, ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
# テキスト画像のリストを作成 # テキスト画像のリストを作成
images = [self.createTextImageMessageType(message_type, date_time)] images = [self.createTextImageMessageType(message_type, date_time)]
@@ -430,20 +485,25 @@ class OverlayImage:
if translation and target_language: if translation and target_language:
# 元のメッセージがある場合は小さいサイズで追加 # 元のメッセージがある場合は小さいサイズで追加
if message is not None: if message is not None:
images.append( small_img = self.createTextImageLargeLog(message_type, "small", message, your_language)
self.createTextImageLargeLog(message_type, "small", message, your_language) images.append(small_img)
)
# 翻訳をすべて大きいサイズで追加 # 翻訳をすべて大きいサイズで追加
for trans, lang in zip(translation, target_language): for trans, lang, translite in zip(translation, target_language, transliteration_translation):
images.append( try:
self.createTextImageLargeLog(message_type, "large", trans, lang) large_img = self.createTextboxLargeLogWithRubyTokens(message_type, "large", trans, translite, lang, ruby_font_scale, ruby_line_spacing)
) except Exception:
errorLogging()
large_img = self.createTextImageLargeLog(message_type, "large", trans, lang)
images.append(large_img)
else: else:
# 翻訳がない場合は元のメッセージのみ # 翻訳がない場合は元のメッセージのみ
images.append( try:
self.createTextImageLargeLog(message_type, "large", message, your_language) large_img = self.createTextboxLargeLogWithRubyTokens(message_type, "large", message, transliteration_message, your_language, ruby_font_scale, ruby_line_spacing)
) except Exception:
errorLogging()
large_img = self.createTextImageLargeLog(message_type, "large", message, your_language)
images.append(large_img)
# すべてのテキスト画像を縦に結合 # すべてのテキスト画像を縦に結合
combined_img = images[0] combined_img = images[0]
@@ -452,7 +512,7 @@ class OverlayImage:
return combined_img return combined_img
def createOverlayImageLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = []) -> Image: def createOverlayImageLargeLog(self, message_type: str, message: Optional[str] = None, your_language: Optional[str] = None, translation: List[str] = [], target_language: List[str] = [], transliteration_message: List[dict] = [], transliteration_translation: List[List[dict]] = [], ruby_font_scale: float = 0.5, ruby_line_spacing: int = 4) -> Image:
ui_color = self.getUiColorLargeLog() ui_color = self.getUiColorLargeLog()
background_color = ui_color["background_color"] background_color = ui_color["background_color"]
background_outline_color = ui_color["background_outline_color"] background_outline_color = ui_color["background_outline_color"]
@@ -468,6 +528,8 @@ class OverlayImage:
"your_language": your_language, "your_language": your_language,
"translation": translation, "translation": translation,
"target_language": target_language, "target_language": target_language,
"transliteration_message": transliteration_message,
"transliteration_translation": transliteration_translation,
"datetime": datetime.now().strftime("%H:%M") "datetime": datetime.now().strftime("%H:%M")
}) })
@@ -481,7 +543,12 @@ class OverlayImage:
log["your_language"], log["your_language"],
log["translation"], log["translation"],
log["target_language"], log["target_language"],
log["datetime"]) for log in self.message_log log["datetime"],
transliteration_message=log.get("transliteration_message", [{}]),
transliteration_translation=log.get("transliteration_translation", [{}]),
ruby_font_scale=ruby_font_scale,
ruby_line_spacing=ruby_line_spacing,
) for log in self.message_log
] ]
img = imgs[0] img = imgs[0]
@@ -499,7 +566,7 @@ class OverlayImage:
if __name__ == "__main__": if __name__ == "__main__":
overlay = OverlayImage() overlay = OverlayImage()
# Basic small log test (with translation list form) # Basic small log test (with translation list form)
img = overlay.createOverlayImageSmallLog("Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"]) img = overlay.createOverlayImageSmallLog("Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], [], [[]])
img.save("overlay_small.png") img.save("overlay_small.png")
# Ruby small log test (Japanese original with transliteration tokens) # Ruby small log test (Japanese original with transliteration tokens)
@@ -510,31 +577,91 @@ if __name__ == "__main__":
# Ruby on original + ruby on translation example # Ruby on original + ruby on translation example
translation_tokens = [ translation_tokens = [
[ [
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"}, {"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"}, {"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
] ]
] ]
img_ruby = overlay.createOverlayImageSmallLog( img_ruby = overlay.createOverlayImageSmallLog(
"慮る", "慮る",
"Japanese", "Japanese",
["慮る"], ["慮る", "慮る", "慮る"],
["Default"], ["Japanese", "Japanese", "Japanese"],
transliteration_tokens=ruby_tokens, transliteration_message=ruby_tokens,
translation_transliteration_tokens=translation_tokens, transliteration_translation=translation_tokens,
ruby_font_scale=0.5, ruby_font_scale=0.5,
ruby_line_spacing=4, ruby_line_spacing=4,
) )
img_ruby.save("overlay_small_ruby.png") img_ruby.save("overlay_small_ruby.png")
# Large log tests (adjusted to pass translation/target_language as lists) # Large log tests (adjusted to pass translation/target_language as lists)
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"]) img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], transliteration_message=[], transliteration_translation=[])
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"]) img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_message=[
long_en = "Hello, World!" + "a"*25 + ""*25 + "a"*25 + ""*25 {"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
long_jp = "こんにちは、世界!" + "a"*25 + ""*25 + "a"*25 + ""*25 {"orig": "", "hira": "", "hepburn": "ru"},
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"]) ], transliteration_translation=[
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en], ["English"]) [
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"]) {"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"]) {"orig": "", "hira": "", "hepburn": "ru"},
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"]) ]
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en], ["English"]) ])
long_en = "Hello, World!"
long_jp = "こんにちは、世界!"
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[[]])
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en], ["English"], transliteration_message=[], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", ["こんにちは、世界!"], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[[]])
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", ["Hello, World!"], ["English"], transliteration_message=[
{"orig": "こんにちは", "hira": "こんにちは", "hepburn": "konnichiha"},
{"orig": "世界", "hira": "sekai", "hepburn": "sekai"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("send", long_en, "English", [long_jp], ["Japanese"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img = overlay.createOverlayImageLargeLog("receive", long_jp, "Japanese", [long_en, long_en, long_en], ["English", "English", "English"], transliteration_message=[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
], transliteration_translation=[
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
],
[
{"orig": "", "hira": "おもんぱか", "hepburn": "omonpaka"},
{"orig": "", "hira": "", "hepburn": "ru"},
]
])
img.save("overlay_large.png") img.save("overlay_large.png")