去年學著使用Manga-Imgae-Translator
雖然已經玩到要改程式這步了,但玩完後就沒在使用了
主要還是沒有明確要使用的目標
就沒有什麼動力了
今年有了個目標,就重開要來好好用用
然後對程式的修改就越改越多了...........................................
與去年使用隔了一陣子,這專案應該有更新
在進行了比對目前跟去年的專案後發現
新舊版差異太大,放棄用更新的
畢竟現在多了個新功能MangaStudio之外
套件也整個不一樣了
requirments.txt裡paddle ocr也沒在用了
稍稍查了一下,有ocr字樣的套件多了個manga-ocr,大概是用這個吧
所以就把原本的專案連同虛擬環境砍了,重新下載現在的版本來安裝
那因為新版的沒有paddle ocr的cuda版本限制
安裝pytorch時,就不用理cuda版本,直接更新到比較新一點點的cuda126版了
也同步把這個訊息更新到去年寫的那一篇上面
在正式使用前,有先問過AI一輪,要怎麼設定會讓成果更好
畢竟在文字嵌入上它還是會出錯,想要到修圖就很煩
當然要儘量壓低出錯的機率
那AI也給了我不少建議
其中一項事後補救的建議讓我眼睛為之一亮
那就是將檔案匯出成有分圖層功能的PSD與XCF,方便後製再修改
看起來好像很棒,這功能一定要好好試試
不過還要是耐著性子,先試原本的基礎功能
重裝了現在版本後
發現很在意的文字嵌入功能又更準確了
舊版的準度如果是已達八成五的話,新的大概又拉高到九成五的感受
原本事前問好AI要如何微調的選項,似乎都派不上用途了
而失敗的部分,看起來都比較像OCR失敗所以嵌入才會失敗
字都已經抓錯了,那放回去就很難正常
所以稍稍測試後,就往匯出PSD與XCF版的方向前進,這樣產出會更完美
然後就碰壁卡一個下午了.........
算我沒學到教訓,又沒看說明書
一開始我只看AI建議就下指令要做PSD檔匯出
結果用下去出錯,訊息顯示要裝GIMP才能用
那就下載最新版GIMP 3.2版要來用
這步也是錯的,查下去看到程式碼與說明文件才發現
目前Manga-Image-Translator轉這兩個檔案的功能要靠舊的GIMP 2.X
然後我也不清楚是哪個2.X版,因為我下載了2.9裝上去後還是不行
弄了好一陣子沒成果,我就「狂起來了」(台語)
直接把rendering資料夾下原始碼gimp_render.py給丟上Gemini跟Claude
叫它們直接幫我改成可以支援目前GIMP最新版3.2的程式
幾個來來回回,因為Gemini的都只給我片段
所以弄到一半,就全靠Claude修改了,它會給完整的檔案,直接複製貼上省事 XD
很快地,PSD輸出弄好了
然後用GIMP打開後,發現不如我所願
圖層是都分開了,文字位置也還蠻準的
但PSD的文字圖層,還是圖,所以不能當純文字框直修改處理,要塗消後再輸入
那來轉XCF看看好了
先說結論,轉XCF後,就是我要的功能了
那個圖層是文字型態的圖層,可以直接改文字大小,排列方向,字型與內容
只是沒那麼順利,上面提到卡了一下午,差不多有一半的時間卡在這裡
因為這次AI純靠著錯誤log改不出正確版本,來來回回改了十幾次,同樣的錯誤一直出現
最後是我問AI從哪邊可以查API文件,AI跟我說,我再截圖提供後
才解決了轉3.2版XCF問題[註]
這樣弄出來後,用GIMP打開後,長這樣
這是用現在正紅的間諜家家酒連載這一頁當範例
可以看到文字都正常地在框中
文字圖層的名稱是翻譯前的原文
識別正確的話,就是每一個氣泡框一層
Mask的部分有另外一層
底稿就是原本的圖片
文字圖層就是像Office中的文字框功能一樣
點進去可以很方便地修改其內容
也可以移動,旋轉,放大或縮小框型
有沒有正確識別的文字要蓋掉,並增加嵌字的話
可以先在mask圖層增加塗白的部分
然後看是新增文字圖層還是直接修改舊圖層後就可以解決
太棒了!精修竟然是如此簡單!
只是...一張一張來還是太花時間了 Orz
所以弄了幾張後,還是受不了差不多每張都要修翻譯的狀況
就沒有精度更高的離線翻譯AI模型嗎?
幾個離線模型弄來弄去
最後還是SakuraLLM在專有名詞可以指定上比translategemma好用
只是翻漫畫的精度因為缺乏有脈絡的上下文與情境詞,時不時會偏很大
SakuraLLM的發行版一直停在1.0,這已經是2024年底的產物,要是有更新就好了...
這點在網友的指正下,才知道其實一直有在更新,只是都不是發行(Release)版
就上官網翻了目前比較新的14B-qwen3-v1.5的量化版來用
用這個,因為不是發行版,SakuraLauncherGUI也不支援
只能自行下載最新版的llama.cpp來配合運作
我使用的命令列如下:
因為顯卡只有6GB可用,所以我塞了24層的模型進VRAM,並把輸入的cache進行壓縮
(真想買新電腦,插張有16GB的顯卡)
然後把它寫成BAT批次檔,下次使用直接點就好
理論上使用V1.5版,翻譯器的sakura.py裡prompt的部分,其實要跟著配合修改
不過不更新,用V1.0的prompt,它也翻得出來就是,懶得改的可以不用動它
當然,有更動的話會更好
在實測下,V1.5用專用的prompt還是翻得更好一點
V1.5的prompt長這樣
不想改太大的話,就把v1.0的prompt替換掉就可以用了,像下面這樣
_CHAT_SYSTEM_TEMPLATE_010 = (
#'你是一个轻小说翻译模型,可以流畅通顺地以日本轻小说的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,注意不要擅自添加原文中没有的代词,也不要擅自增加或减少换行。'
'你是一个日本二次元领域的日语翻译模型,可以流畅通顺地以日本轻小说/漫画/Galgame的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,不擅自添加原文中没有的代词。' #換成v1.5
)
當然它的輸出都還是簡體中文,所以想轉繁體中文的話,還是要改個OpenCC來轉
請參考去年那篇要怎麼改
最後來講講新的MangaStudio功能
簡言之就是之前一直都沒有圖形化介面功能
但目前不是很好用
主要是那個佇列操作很不直觀
要先將右側的所有參數設定完成後
然後將匯入的目錄進行排入佇列,這樣才有導入該有的設定
而要排入佇列後,才能按下開始處理,步驟有點繁瑣
不過測試起來最大的問題,還是特定情況下一次就只能出5張圖
看log上是蠻正常處置,因為記憶體不足,所以分批處理,一次5張
只是5張圖產完後,程式就停了...
疑惑之際,換另一組資料夾圖片就又不會了,這下更疑惑了,特定情況的條件是什麼?
反正這次為了GIMP 3.x都改這麼大了,有bug就繼續修就對啦
不過這邊的程式對我來說有點複雜,又是我沒在用的pipeline功能
所以還是餵AI快一點,這辦法確實也很快
依AI指示加完LOG,然後跑個一輪,抓到新方向後,再加新LOG再跑一輪,答案就出來了
在MangaStudio_Data/core/pipeline.py這個程式中
有個功能是抓取開頭為Error:或帶有Traceback (most recent call last)的字串後
就取消接下來的佇列
那解決辦法就很單純了,要嘛加上白名單,忽略己知不會讓程式掛掉的Error
不然就是完全取消這部分的判斷
我自己是走後者啦,修改程式碼如下:
修改第159行
return return_code == 0 #and not has_failed #註解掉後面has_failed的判斷
搞定!
好了,無情的漫畫翻譯機要運作了
之後總算可以看得輕鬆一點了
註:AI修好的原始碼如下
import tempfile
import subprocess
import math
import cv2
import platform
import glob
import os
from ..utils import Context
# convert alignment/direction to gimp's values
alignment_to_justification = {
"left": "TEXT-JUSTIFY-LEFT",
"right": "TEXT-JUSTIFY-RIGHT",
"center": "TEXT-JUSTIFY-CENTER",
}
direction_to_base_direction = {
"h": "TEXT-DIRECTION-LTR",
"v": "TEXT-DIRECTION-TTB-RTL-UPRIGHT",
"hr": "TEXT-DIRECTION-RTL",
"vr": "TEXT-DIRECTION-TTB-LTR-UPRIGHT",
}
# GIMP 3: gimp-text-layer-new signature is (image text font size unit)
# - font: now a GimpFont resource object, NOT a string. Must use
# (car (gimp-font-get-by-name "name")) to convert string to resource (v2 dialect).
# - unit: must be UNIT-PIXEL (was just 0 in GIMP 2).
text_init_template = '( text{n} ( car ( gimp-text-layer-new image "{text}" ( car ( gimp-font-get-by-name "{default_font}" ) ) {text_size} UNIT-PIXEL ) ) )'
# GIMP 3: fonts are now resource objects; use gimp-font-get-by-name to convert
# a font name string into a font resource before passing to gimp-text-layer-set-font.
# In v2 dialect, single return values are wrapped in a list, so use car.
font_template = '( gimp-text-layer-set-font text{n} ( car ( gimp-font-get-by-name "{font}" ) ) )'
angle_template = "( gimp-item-transform-rotate text{n} {angle} TRUE 0 0 )"
# GIMP 3: gimp-image-add-layer is replaced by gimp-image-insert-layer
# which takes (image layer parent position). Use -1 for parent (no parent group)
# and 0 for position (top).
# GIMP 3: gimp-text-layer-set-color still accepts (list R G B) in Script-Fu.
text_template = """
( gimp-image-insert-layer image text{n} -1 0 )
( gimp-text-layer-set-color text{n} (list {color}) )
( gimp-item-set-name text{n} "{name}" )
( gimp-text-layer-set-language text{n} "{language}" )
( gimp-text-layer-set-letter-spacing text{n} {letter_spacing} )
( gimp-text-layer-set-line-spacing text{n} {line_spacing} )
( gimp-text-layer-set-base-direction text{n} {base_direction} )
( gimp-text-layer-set-justification text{n} {justify} )
( gimp-layer-set-offsets text{n} {position} )
( gimp-text-layer-resize text{n} {size} )
{font}
{angle}
"""
# GIMP 3: Non-XCF save procedures were renamed from file-*-save to file-*-export.
# gimp-file-save requires GimpExportOptions which Script-Fu cannot create.
# gimp-xcf-save: internal procedure, GIMP 3 signature is
# (gimp-xcf-save image num-drawables drawables file)
# Use the 'layers' variable (from gimp-image-get-layers) defined in script_template.
# file-psd-export / file-pdf-export: plug-in procedures, use keyword syntax.
save_templates = {
"xcf": '( gimp-xcf-save RUN-NONINTERACTIVE image "{out_file}" )',
"psd": '( file-psd-export #:run-mode RUN-NONINTERACTIVE #:image image #:file "{out_file}" )',
"pdf": '( file-pdf-export #:run-mode RUN-NONINTERACTIVE #:image image #:file "{out_file}" )',
}
# GIMP 3: gimp-file-load-layer uses single filename (GFile)
create_mask = '( inpainting ( car ( gimp-file-load-layer RUN-NONINTERACTIVE image "{mask_file}" ) ) )'
# GIMP 3: gimp-image-insert-layer replaces gimp-image-add-layer
rename_mask = '( gimp-image-insert-layer image inpainting -1 0 ) ( gimp-item-set-name inpainting "mask" )'
# GIMP 3: gimp-file-load uses single filename (GFile, one string instead of two).
# gimp-image-get-layers returns a vector in v3; use (vector-ref layers 0) to get first layer.
# gimp-item-set-lock-content replaced by gimp-item-set-lock-content (still exists in GIMP 3
# but the boolean TRUE should still work in v2 dialect batch mode).
script_template = """
( let* (
( image ( car ( gimp-file-load RUN-NONINTERACTIVE "{input_file}" ) ) )
( layers ( car ( gimp-image-get-layers image ) ) )
( background_layer ( vector-ref layers 0 ) )
{create_mask}
{text_init}
)
{rename_mask}
( gimp-item-set-name background_layer "original image" )
{text}
{save}
( gimp-quit 0 )
)"""
def gimp_render(out_file, ctx: Context):
input_file = os.path.join(tempfile.gettempdir(), ".gimp_input.png")
mask_file = os.path.join(tempfile.gettempdir(), ".gimp_mask.png")
extension = out_file.split(".")[-1]
ctx.upscaled.save(input_file)
# If there is no text on the page, gimp_mask will be None and there is no
# need to add it as a layer.
if ctx.gimp_mask is not None:
cv2.imwrite(mask_file, ctx.gimp_mask)
else:
ctx.text_regions = []
filtered_text_regions = [
text_region for text_region in ctx.text_regions if text_region.translation != ""
]
text_init = "\n".join(
[
text_init_template.format(
n=n,
text=text_region.translation.replace('"', '\\"'),
text_size=text_region.font_size,
default_font=ctx.gimp_font
+ (" Bold" if text_region.bold else "")
+ (" Italic" if text_region.italic else ""),
)
for n, text_region in enumerate(filtered_text_regions)
]
)
text = "".join(
[
text_template.format(
n=n,
color=" ".join([str(value) for value in text_region.fg_colors]),
name=" ".join(text_region.text).replace('"', '\\"'),
position=str(text_region.xywh[0]) + " " + str(text_region.xywh[1]),
size=str(text_region.xywh[2]) + " " + str(text_region.xywh[3]),
justify=alignment_to_justification[text_region.alignment],
font=font_template.format(n=n, font=text_region.font_family)
if text_region.font_family != ""
else "",
# rotated text is weird in gimp so we don't do it unless it's over 10 degrees
angle=angle_template.format(n=n, angle=math.radians(text_region.angle))
if abs(text_region.angle) > 10
else "",
language=text_region.target_lang,
line_spacing=text_region.line_spacing,
letter_spacing=text_region.letter_spacing,
base_direction=direction_to_base_direction[text_region.direction],
)
for n, text_region in enumerate(filtered_text_regions)
]
)
# scheme script to be ran by gimp
full_script = script_template.format(
input_file=input_file.replace("\\", "\\\\"),
text_init=text_init,
text=text,
extension=extension,
save=save_templates[extension].format(out_file=out_file.replace("\\", "\\\\")),
create_mask=(
create_mask.format(mask_file=mask_file.replace("\\", "\\\\"))
if ctx.gimp_mask is not None
else ""
),
rename_mask=(rename_mask if ctx.gimp_mask is not None else ""),
)
gimp_batch(full_script)
# Delete Files
os.unlink(input_file)
# Deleting file only if it exists
if os.path.exists(mask_file):
os.unlink(mask_file)
def gimp_console_executable():
"""
Find the GIMP executable.
GIMP 3 removed gimp-console; use 'gimp' with -i (no interface) instead.
On Windows, search for gimp-3.x.exe or gimp.exe in standard locations.
"""
executable = "gimp"
if platform.system() == "Windows":
# Try GIMP 3.x locations first
for env_var in ["LOCALAPPDATA", "ProgramFiles"]:
base = os.getenv(env_var, "")
if not base:
continue
# GIMP 3 installs to "GIMP 3" directory
for gimp_dir_name in ["GIMP 3", "GIMP 2"]:
gimp_dir = os.path.join(base, "Programs", gimp_dir_name, "bin")
if not os.path.isdir(gimp_dir):
gimp_dir = os.path.join(base, gimp_dir_name, "bin")
if not os.path.isdir(gimp_dir):
continue
# GIMP 3: look for gimp-3.x.exe (no separate console binary)
executables = glob.glob(os.path.join(gimp_dir, "gimp-3.*.exe"))
if executables:
return executables[0]
# Fallback: look for gimp-console (GIMP 2 style)
executables = glob.glob(os.path.join(gimp_dir, "gimp-console-*.exe"))
if executables:
return executables[0]
# Generic gimp.exe
generic = os.path.join(gimp_dir, "gimp.exe")
if os.path.isfile(generic):
return generic
print("error: gimp not found in standard Windows directories")
return executable
def gimp_batch(script):
"""
Run a gimp script in batch mode.
GIMP 3: gimp-console was removed. Use 'gimp -i' (no interface) instead.
GIMP 3: Must specify --batch-interpreter=plug-in-script-fu-eval explicitly.
"""
executable = gimp_console_executable()
if executable is None:
raise Exception("GIMP executable not found")
result = subprocess.run(
[
executable,
"-i", # no UI
"--batch-interpreter=plug-in-script-fu-eval", # GIMP 3 requires this
"-b", script,
"-b", "(gimp-quit 0)",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
print("=== Output")
print(result.stdout)
print("=== Error")
print(result.stderr)
# GIMP 3 produces harmless GEGL warnings on exit (buffer leaks, cache warnings).
# Only raise on actual Script-Fu execution errors.
if result.stderr:
# Filter out known harmless GIMP 3 warnings
error_lines = []
for line in result.stderr.splitlines():
# Skip known harmless warnings
if any(skip in line for skip in [
"GEGL-",
"GeglBuffer",
"gegl_tile",
"GEGL_DEBUG",
"dbind-WARNING",
"AT-SPI",
"LibGimp-WARNING",
"scriptfu-WARNING",
"g_queue_is_empty",
]):
continue
error_lines.append(line)
# Check remaining lines for actual Script-Fu errors
filtered_stderr = "\n".join(error_lines)
if "Error:" in filtered_stderr or "batch command experienced an execution error" in filtered_stderr:
raise Exception("GIMP Execution error")










沒有留言:
張貼留言