Add manga URL colorizer tab with cache preview and batch colorization
Browse files- .gitignore +9 -0
- README.md +7 -1
- app.py +263 -3
- manga_colorizer.py +360 -0
- requirements.txt +3 -1
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cache/
|
| 2 |
+
outputs/
|
| 3 |
+
downloads/
|
| 4 |
+
manga_cache/
|
| 5 |
+
*.zip
|
| 6 |
+
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.py[cod]
|
| 9 |
+
.pytest_cache/
|
README.md
CHANGED
|
@@ -11,4 +11,10 @@ license: apache-2.0
|
|
| 11 |
short_description: FireRed-Image-Edit × Qwen-Image-Edit-Rapid (Transformers)
|
| 12 |
---
|
| 13 |
|
| 14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
short_description: FireRed-Image-Edit × Qwen-Image-Edit-Rapid (Transformers)
|
| 12 |
---
|
| 13 |
|
| 14 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 15 |
+
|
| 16 |
+
## Manga URL Colorizer
|
| 17 |
+
|
| 18 |
+
The Space now includes a second tab for manga page extraction and batch colorization from allowed URLs. It caches original pages under `cache/manga/<slug>/original/`, saves generated pages under `cache/manga/<slug>/colorized/`, and produces a downloadable `colorized_manga.zip`.
|
| 19 |
+
|
| 20 |
+
Use only content you own or have permission to process. The URL fetcher is limited to configured allowed domains and does not bypass login, captcha, paywall, Cloudflare, or other access protections.
|
app.py
CHANGED
|
@@ -11,6 +11,17 @@ import html as html_lib
|
|
| 11 |
from io import BytesIO
|
| 12 |
from PIL import Image
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
MAX_SEED = np.iinfo(np.int32).max
|
| 15 |
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
|
| 16 |
|
|
@@ -215,6 +226,126 @@ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps,
|
|
| 215 |
torch.cuda.empty_cache()
|
| 216 |
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
css = r"""
|
| 219 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
| 220 |
*{box-sizing:border-box;margin:0;padding:0}
|
|
@@ -225,6 +356,20 @@ body,.gradio-container{
|
|
| 225 |
.dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
|
| 226 |
footer{display:none!important}
|
| 227 |
.hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
#example-load-btn{
|
| 229 |
position:absolute!important;left:-9999px!important;top:-9999px!important;
|
| 230 |
width:1px!important;height:1px!important;opacity:0.01!important;
|
|
@@ -988,7 +1133,9 @@ with gr.Blocks() as demo:
|
|
| 988 |
example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
|
| 989 |
example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
|
| 990 |
|
| 991 |
-
gr.
|
|
|
|
|
|
|
| 992 |
<div class="app-shell">
|
| 993 |
<div class="app-header">
|
| 994 |
<div class="app-header-left">
|
|
@@ -1133,6 +1280,96 @@ with gr.Blocks() as demo:
|
|
| 1133 |
</div>
|
| 1134 |
""")
|
| 1135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1136 |
run_btn = gr.Button("Run", elem_id="gradio-run-btn")
|
| 1137 |
|
| 1138 |
demo.load(fn=None, js=gallery_js)
|
|
@@ -1159,11 +1396,34 @@ with gr.Blocks() as demo:
|
|
| 1159 |
queue=False,
|
| 1160 |
)
|
| 1161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
if __name__ == "__main__":
|
| 1163 |
demo.queue(max_size=50).launch(
|
| 1164 |
css=css,
|
| 1165 |
mcp_server=True,
|
| 1166 |
ssr_mode=False,
|
| 1167 |
show_error=True,
|
| 1168 |
-
allowed_paths=["examples"],
|
| 1169 |
-
)
|
|
|
|
| 11 |
from io import BytesIO
|
| 12 |
from PIL import Image
|
| 13 |
|
| 14 |
+
from manga_colorizer import (
|
| 15 |
+
MANGA_CACHE_ROOT,
|
| 16 |
+
MANGA_DEFAULT_PROMPT,
|
| 17 |
+
MANGA_NEGATIVE_PROMPT,
|
| 18 |
+
MangaColorizerError,
|
| 19 |
+
extract_and_cache_manga,
|
| 20 |
+
get_colorized_path,
|
| 21 |
+
load_manga_metadata,
|
| 22 |
+
make_colorized_zip,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
MAX_SEED = np.iinfo(np.int32).max
|
| 26 |
LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
|
| 27 |
|
|
|
|
| 226 |
torch.cuda.empty_cache()
|
| 227 |
|
| 228 |
|
| 229 |
+
def extract_manga_for_ui(url, max_pages):
|
| 230 |
+
try:
|
| 231 |
+
status, original_paths, metadata_json = extract_and_cache_manga(url, max_pages)
|
| 232 |
+
choices = [f"{idx:03d}" for idx in range(1, len(original_paths) + 1)]
|
| 233 |
+
default_selection = choices[:1]
|
| 234 |
+
return (
|
| 235 |
+
status,
|
| 236 |
+
original_paths,
|
| 237 |
+
metadata_json,
|
| 238 |
+
gr.update(choices=choices, value=default_selection),
|
| 239 |
+
)
|
| 240 |
+
except MangaColorizerError as exc:
|
| 241 |
+
raise gr.Error(str(exc)) from exc
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _parse_selected_page_indexes(selected_pages, total_pages):
|
| 245 |
+
if isinstance(selected_pages, str):
|
| 246 |
+
selected_pages = [selected_pages]
|
| 247 |
+
indexes = []
|
| 248 |
+
for page in selected_pages or []:
|
| 249 |
+
try:
|
| 250 |
+
page_number = int(str(page).split()[0])
|
| 251 |
+
except (TypeError, ValueError):
|
| 252 |
+
continue
|
| 253 |
+
index = page_number - 1
|
| 254 |
+
if 0 <= index < total_pages and index not in indexes:
|
| 255 |
+
indexes.append(index)
|
| 256 |
+
return indexes
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def colorize_manga_page(image_path: str, prompt: str, seed: int, guidance_scale: float, steps: int) -> Image.Image:
|
| 260 |
+
input_image = Image.open(image_path).convert("RGB")
|
| 261 |
+
width, height = update_dimensions_on_upload(input_image)
|
| 262 |
+
generator = torch.Generator(device=device).manual_seed(int(seed))
|
| 263 |
+
result_image = pipe(
|
| 264 |
+
image=[input_image],
|
| 265 |
+
prompt=prompt,
|
| 266 |
+
negative_prompt=MANGA_NEGATIVE_PROMPT,
|
| 267 |
+
height=height,
|
| 268 |
+
width=width,
|
| 269 |
+
num_inference_steps=int(steps),
|
| 270 |
+
generator=generator,
|
| 271 |
+
true_cfg_scale=float(guidance_scale),
|
| 272 |
+
).images[0]
|
| 273 |
+
output_path = get_colorized_path(image_path)
|
| 274 |
+
result_image.save(output_path)
|
| 275 |
+
return result_image
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
@spaces.GPU
|
| 279 |
+
def colorize_cached_manga(
|
| 280 |
+
metadata_json,
|
| 281 |
+
prompt,
|
| 282 |
+
selected_pages,
|
| 283 |
+
colorize_all,
|
| 284 |
+
seed,
|
| 285 |
+
randomize_seed,
|
| 286 |
+
guidance_scale,
|
| 287 |
+
steps,
|
| 288 |
+
max_color_pages,
|
| 289 |
+
progress=gr.Progress(track_tqdm=True),
|
| 290 |
+
):
|
| 291 |
+
if not torch.cuda.is_available():
|
| 292 |
+
raise gr.Error("No hay GPU disponible para colorear manga.")
|
| 293 |
+
|
| 294 |
+
try:
|
| 295 |
+
metadata = load_manga_metadata(metadata_json)
|
| 296 |
+
except MangaColorizerError as exc:
|
| 297 |
+
raise gr.Error(str(exc)) from exc
|
| 298 |
+
|
| 299 |
+
original_paths = metadata.get("original_paths") or []
|
| 300 |
+
existing_paths = [path for path in original_paths if path and os.path.exists(path)]
|
| 301 |
+
if not existing_paths:
|
| 302 |
+
raise gr.Error("No hay imagenes en cache. Extrae las paginas primero.")
|
| 303 |
+
|
| 304 |
+
try:
|
| 305 |
+
limit = int(max_color_pages)
|
| 306 |
+
except (TypeError, ValueError):
|
| 307 |
+
limit = 5
|
| 308 |
+
limit = max(1, min(limit, 30))
|
| 309 |
+
|
| 310 |
+
if colorize_all:
|
| 311 |
+
page_indexes = list(range(min(len(existing_paths), limit)))
|
| 312 |
+
else:
|
| 313 |
+
page_indexes = _parse_selected_page_indexes(selected_pages, len(existing_paths))[:limit]
|
| 314 |
+
|
| 315 |
+
if not page_indexes:
|
| 316 |
+
raise gr.Error("Selecciona al menos una pagina o activa colorear todas.")
|
| 317 |
+
|
| 318 |
+
prompt = (prompt or MANGA_DEFAULT_PROMPT).strip() or MANGA_DEFAULT_PROMPT
|
| 319 |
+
try:
|
| 320 |
+
base_seed = int(seed)
|
| 321 |
+
except (TypeError, ValueError):
|
| 322 |
+
base_seed = 0
|
| 323 |
+
|
| 324 |
+
colorized_paths = []
|
| 325 |
+
total = len(page_indexes)
|
| 326 |
+
for progress_index, page_index in enumerate(page_indexes, start=1):
|
| 327 |
+
page_seed = random.randint(0, MAX_SEED) if randomize_seed else base_seed
|
| 328 |
+
page_path = existing_paths[page_index]
|
| 329 |
+
progress(progress_index / total, desc=f"Colorizing page {page_index + 1:03d}")
|
| 330 |
+
try:
|
| 331 |
+
colorize_manga_page(page_path, prompt, page_seed, guidance_scale, steps)
|
| 332 |
+
finally:
|
| 333 |
+
gc.collect()
|
| 334 |
+
torch.cuda.empty_cache()
|
| 335 |
+
colorized_paths.append(str(get_colorized_path(page_path)))
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
zip_path = make_colorized_zip(colorized_paths)
|
| 339 |
+
except MangaColorizerError as exc:
|
| 340 |
+
raise gr.Error(str(exc)) from exc
|
| 341 |
+
|
| 342 |
+
status = (
|
| 343 |
+
f"Listo: {len(colorized_paths)} pagina(s) coloreada(s). "
|
| 344 |
+
f"ZIP generado. Solo usar con contenido propio o con permiso."
|
| 345 |
+
)
|
| 346 |
+
return status, colorized_paths, zip_path
|
| 347 |
+
|
| 348 |
+
|
| 349 |
css = r"""
|
| 350 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
| 351 |
*{box-sizing:border-box;margin:0;padding:0}
|
|
|
|
| 356 |
.dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
|
| 357 |
footer{display:none!important}
|
| 358 |
.hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
|
| 359 |
+
.manga-colorizer-shell{
|
| 360 |
+
max-width:1200px;margin:12px auto 24px;padding:18px;background:#18181b;
|
| 361 |
+
border:1px solid #27272a;border-radius:12px;box-shadow:0 18px 36px rgba(0,0,0,.34);
|
| 362 |
+
}
|
| 363 |
+
.manga-colorizer-shell h2{color:#ffffff!important;font-size:22px!important;margin-bottom:6px!important}
|
| 364 |
+
.manga-colorizer-shell p,.manga-colorizer-shell label,.manga-colorizer-shell span{
|
| 365 |
+
color:#d4d4d8!important;
|
| 366 |
+
}
|
| 367 |
+
.manga-colorizer-shell textarea,.manga-colorizer-shell input{
|
| 368 |
+
background:#09090b!important;color:#f4f4f5!important;border-color:#3f3f46!important;
|
| 369 |
+
}
|
| 370 |
+
.manga-colorizer-shell button{
|
| 371 |
+
border-radius:8px!important;font-weight:700!important;
|
| 372 |
+
}
|
| 373 |
#example-load-btn{
|
| 374 |
position:absolute!important;left:-9999px!important;top:-9999px!important;
|
| 375 |
width:1px!important;height:1px!important;opacity:0.01!important;
|
|
|
|
| 1133 |
example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
|
| 1134 |
example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
|
| 1135 |
|
| 1136 |
+
with gr.Tabs(elem_id="main-tabs"):
|
| 1137 |
+
with gr.Tab("Image Editor"):
|
| 1138 |
+
gr.HTML(f"""
|
| 1139 |
<div class="app-shell">
|
| 1140 |
<div class="app-header">
|
| 1141 |
<div class="app-header-left">
|
|
|
|
| 1280 |
</div>
|
| 1281 |
""")
|
| 1282 |
|
| 1283 |
+
with gr.Tab("Manga URL Colorizer"):
|
| 1284 |
+
with gr.Column(elem_classes="manga-colorizer-shell"):
|
| 1285 |
+
gr.Markdown(
|
| 1286 |
+
"## Manga URL Colorizer\n"
|
| 1287 |
+
"Extrae paginas desde una URL permitida, guarda originales en cache, "
|
| 1288 |
+
"previsualiza y colorea por lotes usando el mismo modelo cargado. "
|
| 1289 |
+
"**Solo usar con contenido propio o con permiso.**"
|
| 1290 |
+
)
|
| 1291 |
+
with gr.Row():
|
| 1292 |
+
manga_url = gr.Textbox(
|
| 1293 |
+
label="Manga URL",
|
| 1294 |
+
placeholder="https://hmanga.asia/porque-eres-tu-enano/#all",
|
| 1295 |
+
scale=4,
|
| 1296 |
+
)
|
| 1297 |
+
manga_max_pages = gr.Slider(
|
| 1298 |
+
label="Maximo de paginas a descargar",
|
| 1299 |
+
minimum=1,
|
| 1300 |
+
maximum=120,
|
| 1301 |
+
step=1,
|
| 1302 |
+
value=10,
|
| 1303 |
+
scale=1,
|
| 1304 |
+
)
|
| 1305 |
+
manga_extract_btn = gr.Button("1. Extraer imagenes", variant="primary")
|
| 1306 |
+
manga_status = gr.Markdown("Ready.")
|
| 1307 |
+
manga_original_gallery = gr.Gallery(
|
| 1308 |
+
label="Preview de paginas originales",
|
| 1309 |
+
columns=5,
|
| 1310 |
+
height=420,
|
| 1311 |
+
object_fit="contain",
|
| 1312 |
+
)
|
| 1313 |
+
manga_metadata = gr.Textbox(label="Manga metadata", visible=False)
|
| 1314 |
+
|
| 1315 |
+
with gr.Row():
|
| 1316 |
+
manga_selected_pages = gr.CheckboxGroup(
|
| 1317 |
+
label="Paginas a colorear",
|
| 1318 |
+
choices=[],
|
| 1319 |
+
value=[],
|
| 1320 |
+
scale=3,
|
| 1321 |
+
)
|
| 1322 |
+
manga_colorize_all = gr.Checkbox(
|
| 1323 |
+
label="Colorear todas las paginas descargadas",
|
| 1324 |
+
value=False,
|
| 1325 |
+
scale=1,
|
| 1326 |
+
)
|
| 1327 |
+
|
| 1328 |
+
manga_prompt = gr.Textbox(
|
| 1329 |
+
label="Prompt de colorizacion",
|
| 1330 |
+
value=MANGA_DEFAULT_PROMPT,
|
| 1331 |
+
lines=6,
|
| 1332 |
+
)
|
| 1333 |
+
with gr.Row():
|
| 1334 |
+
manga_seed = gr.Slider(
|
| 1335 |
+
label="Seed",
|
| 1336 |
+
minimum=0,
|
| 1337 |
+
maximum=MAX_SEED,
|
| 1338 |
+
step=1,
|
| 1339 |
+
value=0,
|
| 1340 |
+
)
|
| 1341 |
+
manga_randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
|
| 1342 |
+
with gr.Row():
|
| 1343 |
+
manga_guidance_scale = gr.Slider(
|
| 1344 |
+
label="Guidance scale",
|
| 1345 |
+
minimum=1.0,
|
| 1346 |
+
maximum=10.0,
|
| 1347 |
+
step=0.1,
|
| 1348 |
+
value=1.0,
|
| 1349 |
+
)
|
| 1350 |
+
manga_steps = gr.Slider(
|
| 1351 |
+
label="Steps",
|
| 1352 |
+
minimum=1,
|
| 1353 |
+
maximum=50,
|
| 1354 |
+
step=1,
|
| 1355 |
+
value=4,
|
| 1356 |
+
)
|
| 1357 |
+
manga_max_color_pages = gr.Slider(
|
| 1358 |
+
label="Maximo de paginas a colorear",
|
| 1359 |
+
minimum=1,
|
| 1360 |
+
maximum=30,
|
| 1361 |
+
step=1,
|
| 1362 |
+
value=5,
|
| 1363 |
+
)
|
| 1364 |
+
manga_colorize_btn = gr.Button("2. Colorear manga", variant="primary")
|
| 1365 |
+
manga_colorized_gallery = gr.Gallery(
|
| 1366 |
+
label="Resultados coloreados",
|
| 1367 |
+
columns=5,
|
| 1368 |
+
height=420,
|
| 1369 |
+
object_fit="contain",
|
| 1370 |
+
)
|
| 1371 |
+
manga_zip = gr.File(label="Descargar ZIP coloreado")
|
| 1372 |
+
|
| 1373 |
run_btn = gr.Button("Run", elem_id="gradio-run-btn")
|
| 1374 |
|
| 1375 |
demo.load(fn=None, js=gallery_js)
|
|
|
|
| 1396 |
queue=False,
|
| 1397 |
)
|
| 1398 |
|
| 1399 |
+
manga_extract_btn.click(
|
| 1400 |
+
fn=extract_manga_for_ui,
|
| 1401 |
+
inputs=[manga_url, manga_max_pages],
|
| 1402 |
+
outputs=[manga_status, manga_original_gallery, manga_metadata, manga_selected_pages],
|
| 1403 |
+
queue=False,
|
| 1404 |
+
)
|
| 1405 |
+
|
| 1406 |
+
manga_colorize_btn.click(
|
| 1407 |
+
fn=colorize_cached_manga,
|
| 1408 |
+
inputs=[
|
| 1409 |
+
manga_metadata,
|
| 1410 |
+
manga_prompt,
|
| 1411 |
+
manga_selected_pages,
|
| 1412 |
+
manga_colorize_all,
|
| 1413 |
+
manga_seed,
|
| 1414 |
+
manga_randomize_seed,
|
| 1415 |
+
manga_guidance_scale,
|
| 1416 |
+
manga_steps,
|
| 1417 |
+
manga_max_color_pages,
|
| 1418 |
+
],
|
| 1419 |
+
outputs=[manga_status, manga_colorized_gallery, manga_zip],
|
| 1420 |
+
)
|
| 1421 |
+
|
| 1422 |
if __name__ == "__main__":
|
| 1423 |
demo.queue(max_size=50).launch(
|
| 1424 |
css=css,
|
| 1425 |
mcp_server=True,
|
| 1426 |
ssr_mode=False,
|
| 1427 |
show_error=True,
|
| 1428 |
+
allowed_paths=["examples", str(MANGA_CACHE_ROOT)],
|
| 1429 |
+
)
|
manga_colorizer.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import ipaddress
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import zipfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from urllib.parse import unquote, urljoin, urlparse, urlunparse
|
| 8 |
+
|
| 9 |
+
import requests
|
| 10 |
+
from bs4 import BeautifulSoup
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
ALLOWED_DOMAINS = ["hmanga.asia"]
|
| 14 |
+
MANGA_CACHE_ROOT = Path("cache") / "manga"
|
| 15 |
+
MANGA_IMAGE_EXTENSIONS = (".webp", ".jpg", ".jpeg", ".png")
|
| 16 |
+
MAX_MANGA_PAGES = 120
|
| 17 |
+
MAX_IMAGE_BYTES = 35 * 1024 * 1024
|
| 18 |
+
MAX_REDIRECTS = 5
|
| 19 |
+
|
| 20 |
+
MANGA_DEFAULT_PROMPT = (
|
| 21 |
+
"Colorize this black and white manga page into a clean modern anime/manhwa color style. "
|
| 22 |
+
"Preserve the original line art, panel layout, speech bubbles, text, character identity, "
|
| 23 |
+
"clothing, pose, anatomy, camera angle, background, and composition. Do not crop, redraw, "
|
| 24 |
+
"remove, or add panels. Keep all text readable and unchanged. Apply natural skin tones, "
|
| 25 |
+
"soft shadows, vibrant but controlled colors, clean cel shading, subtle highlights, detailed "
|
| 26 |
+
"hair coloring, and harmonious background colors. Maintain the original manga style and avoid "
|
| 27 |
+
"overpainting the ink lines. Do not sexualize the characters, do not add nudity, do not change "
|
| 28 |
+
"clothing coverage, and do not alter the body proportions."
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
MANGA_NEGATIVE_PROMPT = (
|
| 32 |
+
"worst quality, low quality, blurry, cropped, extra panels, missing panels, unreadable text, "
|
| 33 |
+
"changed text, watermark, signature, deformed anatomy, extra limbs, missing limbs, overpainted "
|
| 34 |
+
"lineart, messy colors, nudity, sexualized, changed outfit, changed pose"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
REQUEST_HEADERS = {
|
| 38 |
+
"User-Agent": "Mozilla/5.0",
|
| 39 |
+
"Referer": "https://hmanga.asia/",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class MangaColorizerError(ValueError):
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _normalize_host(hostname):
|
| 48 |
+
if not hostname:
|
| 49 |
+
return ""
|
| 50 |
+
return hostname.strip().strip("[]").lower().rstrip(".")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _is_blocked_host(hostname):
|
| 54 |
+
host = _normalize_host(hostname)
|
| 55 |
+
if host in {"localhost", "local", "0", "0.0.0.0"}:
|
| 56 |
+
return True
|
| 57 |
+
try:
|
| 58 |
+
ip = ipaddress.ip_address(host)
|
| 59 |
+
except ValueError:
|
| 60 |
+
return False
|
| 61 |
+
return any(
|
| 62 |
+
[
|
| 63 |
+
ip.is_loopback,
|
| 64 |
+
ip.is_private,
|
| 65 |
+
ip.is_link_local,
|
| 66 |
+
ip.is_multicast,
|
| 67 |
+
ip.is_reserved,
|
| 68 |
+
ip.is_unspecified,
|
| 69 |
+
]
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _is_allowed_domain(hostname):
|
| 74 |
+
host = _normalize_host(hostname)
|
| 75 |
+
return any(host == domain or host.endswith(f".{domain}") for domain in ALLOWED_DOMAINS)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def sanitize_url(url: str) -> str:
|
| 79 |
+
if not url or not str(url).strip():
|
| 80 |
+
raise MangaColorizerError("URL invalida: pega una URL http/https.")
|
| 81 |
+
|
| 82 |
+
parsed = urlparse(str(url).strip())
|
| 83 |
+
if parsed.scheme not in {"http", "https"}:
|
| 84 |
+
raise MangaColorizerError("URL invalida: solo se permite http o https.")
|
| 85 |
+
if parsed.username or parsed.password:
|
| 86 |
+
raise MangaColorizerError("URL invalida: no uses credenciales embebidas.")
|
| 87 |
+
|
| 88 |
+
host = _normalize_host(parsed.hostname)
|
| 89 |
+
if _is_blocked_host(host):
|
| 90 |
+
raise MangaColorizerError("URL bloqueada por seguridad.")
|
| 91 |
+
if not _is_allowed_domain(host):
|
| 92 |
+
allowed = ", ".join(ALLOWED_DOMAINS)
|
| 93 |
+
raise MangaColorizerError(f"Dominio no permitido. Dominios permitidos: {allowed}.")
|
| 94 |
+
|
| 95 |
+
if parsed.port and parsed.port not in {80, 443}:
|
| 96 |
+
raise MangaColorizerError("URL invalida: puerto no permitido.")
|
| 97 |
+
|
| 98 |
+
path = parsed.path or "/"
|
| 99 |
+
netloc = host
|
| 100 |
+
if parsed.port:
|
| 101 |
+
netloc = f"{netloc}:{parsed.port}"
|
| 102 |
+
return urlunparse((parsed.scheme, netloc, path, "", parsed.query, parsed.fragment))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def get_manga_slug(url: str) -> str:
|
| 106 |
+
clean_url = sanitize_url(url)
|
| 107 |
+
parsed = urlparse(clean_url)
|
| 108 |
+
parts = [part for part in unquote(parsed.path).split("/") if part]
|
| 109 |
+
base = parts[-1] if parts else "manga"
|
| 110 |
+
slug = re.sub(r"[^a-zA-Z0-9]+", "-", base).strip("-").lower() or "manga"
|
| 111 |
+
slug = slug[:80].strip("-") or "manga"
|
| 112 |
+
short_hash = hashlib.sha256(clean_url.encode("utf-8")).hexdigest()[:8]
|
| 113 |
+
return f"{slug}-{short_hash}"
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _looks_like_blocked_page(html: str) -> bool:
|
| 117 |
+
lowered = html.lower()
|
| 118 |
+
hard_markers = [
|
| 119 |
+
"cf-chl",
|
| 120 |
+
"cloudflare",
|
| 121 |
+
"turnstile",
|
| 122 |
+
"captcha",
|
| 123 |
+
"verify you are human",
|
| 124 |
+
"attention required",
|
| 125 |
+
"just a moment",
|
| 126 |
+
"please enable cookies",
|
| 127 |
+
]
|
| 128 |
+
if any(marker in lowered for marker in hard_markers):
|
| 129 |
+
return True
|
| 130 |
+
has_login_form = "<form" in lowered and ("login" in lowered or "password" in lowered)
|
| 131 |
+
has_manga_images = "/wp-content/uploads/mangas/" in lowered
|
| 132 |
+
return has_login_form and not has_manga_images
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _get_with_validated_redirects(url, *, headers=None, stream=False, timeout=20):
|
| 136 |
+
current_url = sanitize_url(url)
|
| 137 |
+
request_headers = headers or REQUEST_HEADERS
|
| 138 |
+
for _ in range(MAX_REDIRECTS + 1):
|
| 139 |
+
response = requests.get(
|
| 140 |
+
current_url,
|
| 141 |
+
headers=request_headers,
|
| 142 |
+
timeout=timeout,
|
| 143 |
+
allow_redirects=False,
|
| 144 |
+
stream=stream,
|
| 145 |
+
)
|
| 146 |
+
if response.is_redirect or response.is_permanent_redirect:
|
| 147 |
+
location = response.headers.get("Location")
|
| 148 |
+
response.close()
|
| 149 |
+
if not location:
|
| 150 |
+
raise MangaColorizerError("Redireccion sin destino valido.")
|
| 151 |
+
current_url = sanitize_url(urljoin(current_url, location))
|
| 152 |
+
continue
|
| 153 |
+
return response, current_url
|
| 154 |
+
raise MangaColorizerError("Demasiadas redirecciones.")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def fetch_manga_html(url: str) -> str:
|
| 158 |
+
clean_url = sanitize_url(url)
|
| 159 |
+
try:
|
| 160 |
+
response, _ = _get_with_validated_redirects(clean_url, headers=REQUEST_HEADERS, timeout=20)
|
| 161 |
+
except requests.Timeout as exc:
|
| 162 |
+
raise MangaColorizerError("Timeout al descargar la pagina del manga.") from exc
|
| 163 |
+
except requests.RequestException as exc:
|
| 164 |
+
raise MangaColorizerError(f"Error de red al descargar la pagina: {exc}") from exc
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
if response.status_code == 403:
|
| 168 |
+
raise MangaColorizerError("El sitio devolvio 403. Puede requerir login, captcha o bloqueo.")
|
| 169 |
+
if response.status_code == 404:
|
| 170 |
+
raise MangaColorizerError("El sitio devolvio 404. Revisa que la URL exista.")
|
| 171 |
+
if response.status_code >= 400:
|
| 172 |
+
raise MangaColorizerError(f"El sitio devolvio HTTP {response.status_code}.")
|
| 173 |
+
|
| 174 |
+
html = response.text or ""
|
| 175 |
+
if not html.strip():
|
| 176 |
+
raise MangaColorizerError("La pagina descargada esta vacia.")
|
| 177 |
+
if _looks_like_blocked_page(html):
|
| 178 |
+
raise MangaColorizerError("La pagina parece estar protegida por login, captcha o Cloudflare.")
|
| 179 |
+
return html
|
| 180 |
+
finally:
|
| 181 |
+
response.close()
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _candidate_image_values(img):
|
| 185 |
+
for attr in ("src", "data-src", "data-lazy-src", "data-original"):
|
| 186 |
+
value = img.get(attr)
|
| 187 |
+
if value:
|
| 188 |
+
yield value.strip()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _natural_image_sort_key(url):
|
| 192 |
+
parsed = urlparse(url)
|
| 193 |
+
path = unquote(parsed.path)
|
| 194 |
+
dirname = path.rsplit("/", 1)[0]
|
| 195 |
+
filename = path.rsplit("/", 1)[-1]
|
| 196 |
+
stem = filename.rsplit(".", 1)[0]
|
| 197 |
+
numbers = [int(num) for num in re.findall(r"\d+", stem)]
|
| 198 |
+
return (dirname, 0 if numbers else 1, numbers, filename.lower())
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def extract_manga_image_urls(html: str, base_url: str) -> list[str]:
|
| 202 |
+
if not html or not html.strip():
|
| 203 |
+
return []
|
| 204 |
+
|
| 205 |
+
clean_base_url = sanitize_url(base_url)
|
| 206 |
+
soup = BeautifulSoup(html, "html.parser")
|
| 207 |
+
candidates = []
|
| 208 |
+
for selector in ("img.img-fluid.no-lazy", "div[id] img", "img"):
|
| 209 |
+
candidates.extend(soup.select(selector))
|
| 210 |
+
|
| 211 |
+
seen = set()
|
| 212 |
+
image_urls = []
|
| 213 |
+
for img in candidates:
|
| 214 |
+
for raw_value in _candidate_image_values(img):
|
| 215 |
+
if not raw_value or raw_value.startswith("data:"):
|
| 216 |
+
continue
|
| 217 |
+
try:
|
| 218 |
+
absolute_url = sanitize_url(urljoin(clean_base_url, raw_value))
|
| 219 |
+
except MangaColorizerError:
|
| 220 |
+
continue
|
| 221 |
+
parsed = urlparse(absolute_url)
|
| 222 |
+
path_lower = parsed.path.lower()
|
| 223 |
+
if "/wp-content/uploads/mangas/" not in path_lower:
|
| 224 |
+
continue
|
| 225 |
+
if not path_lower.endswith(MANGA_IMAGE_EXTENSIONS):
|
| 226 |
+
continue
|
| 227 |
+
if absolute_url in seen:
|
| 228 |
+
continue
|
| 229 |
+
seen.add(absolute_url)
|
| 230 |
+
image_urls.append(absolute_url)
|
| 231 |
+
|
| 232 |
+
return sorted(image_urls, key=_natural_image_sort_key)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def download_manga_images(image_urls: list[str], cache_dir: Path, referer: str) -> list[str]:
|
| 236 |
+
cache_dir = Path(cache_dir)
|
| 237 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 238 |
+
local_paths = []
|
| 239 |
+
|
| 240 |
+
for index, image_url in enumerate(image_urls, start=1):
|
| 241 |
+
clean_url = sanitize_url(image_url)
|
| 242 |
+
parsed = urlparse(clean_url)
|
| 243 |
+
ext = Path(parsed.path).suffix.lower()
|
| 244 |
+
if ext not in MANGA_IMAGE_EXTENSIONS:
|
| 245 |
+
ext = ".webp"
|
| 246 |
+
target_path = cache_dir / f"{index:03d}{ext}"
|
| 247 |
+
local_paths.append(str(target_path))
|
| 248 |
+
|
| 249 |
+
if target_path.exists() and target_path.stat().st_size > 0:
|
| 250 |
+
continue
|
| 251 |
+
|
| 252 |
+
headers = dict(REQUEST_HEADERS)
|
| 253 |
+
headers["Referer"] = sanitize_url(referer)
|
| 254 |
+
try:
|
| 255 |
+
response, _ = _get_with_validated_redirects(clean_url, headers=headers, stream=True, timeout=30)
|
| 256 |
+
except requests.Timeout as exc:
|
| 257 |
+
raise MangaColorizerError(f"Timeout al descargar imagen {index:03d}.") from exc
|
| 258 |
+
except requests.RequestException as exc:
|
| 259 |
+
raise MangaColorizerError(f"Error de red al descargar imagen {index:03d}: {exc}") from exc
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
if response.status_code >= 400:
|
| 263 |
+
raise MangaColorizerError(f"HTTP {response.status_code} al descargar imagen {index:03d}.")
|
| 264 |
+
content_length = int(response.headers.get("Content-Length") or 0)
|
| 265 |
+
if content_length > MAX_IMAGE_BYTES:
|
| 266 |
+
raise MangaColorizerError(f"Imagen {index:03d} excede el limite de tamano.")
|
| 267 |
+
|
| 268 |
+
tmp_path = target_path.with_suffix(target_path.suffix + ".part")
|
| 269 |
+
total_bytes = 0
|
| 270 |
+
with tmp_path.open("wb") as handle:
|
| 271 |
+
for chunk in response.iter_content(chunk_size=1024 * 256):
|
| 272 |
+
if not chunk:
|
| 273 |
+
continue
|
| 274 |
+
total_bytes += len(chunk)
|
| 275 |
+
if total_bytes > MAX_IMAGE_BYTES:
|
| 276 |
+
tmp_path.unlink(missing_ok=True)
|
| 277 |
+
raise MangaColorizerError(f"Imagen {index:03d} excede el limite de tamano.")
|
| 278 |
+
handle.write(chunk)
|
| 279 |
+
tmp_path.replace(target_path)
|
| 280 |
+
finally:
|
| 281 |
+
response.close()
|
| 282 |
+
|
| 283 |
+
return local_paths
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def extract_and_cache_manga(url: str, max_pages: int):
|
| 287 |
+
clean_url = sanitize_url(url)
|
| 288 |
+
try:
|
| 289 |
+
page_limit = int(max_pages)
|
| 290 |
+
except (TypeError, ValueError):
|
| 291 |
+
page_limit = 10
|
| 292 |
+
page_limit = max(1, min(page_limit, MAX_MANGA_PAGES))
|
| 293 |
+
|
| 294 |
+
html = fetch_manga_html(clean_url)
|
| 295 |
+
image_urls = extract_manga_image_urls(html, clean_url)
|
| 296 |
+
if not image_urls:
|
| 297 |
+
raise MangaColorizerError("No se encontraron imagenes de manga en la pagina.")
|
| 298 |
+
|
| 299 |
+
limited_urls = image_urls[:page_limit]
|
| 300 |
+
slug = get_manga_slug(clean_url)
|
| 301 |
+
manga_root = MANGA_CACHE_ROOT / slug
|
| 302 |
+
original_dir = manga_root / "original"
|
| 303 |
+
colorized_dir = manga_root / "colorized"
|
| 304 |
+
colorized_dir.mkdir(parents=True, exist_ok=True)
|
| 305 |
+
original_paths = download_manga_images(limited_urls, original_dir, clean_url)
|
| 306 |
+
|
| 307 |
+
metadata = {
|
| 308 |
+
"url": clean_url,
|
| 309 |
+
"slug": slug,
|
| 310 |
+
"original_dir": str(original_dir),
|
| 311 |
+
"colorized_dir": str(colorized_dir),
|
| 312 |
+
"image_urls": limited_urls,
|
| 313 |
+
"original_paths": original_paths,
|
| 314 |
+
}
|
| 315 |
+
status = (
|
| 316 |
+
f"Listo: {len(original_paths)} imagen(es) en cache. "
|
| 317 |
+
f"Detectadas en total: {len(image_urls)}. Solo usar con contenido propio o con permiso."
|
| 318 |
+
)
|
| 319 |
+
return status, original_paths, json.dumps(metadata, indent=2)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def load_manga_metadata(metadata_json: str):
|
| 323 |
+
if not metadata_json or not str(metadata_json).strip():
|
| 324 |
+
raise MangaColorizerError("Primero extrae imagenes para crear metadata.")
|
| 325 |
+
try:
|
| 326 |
+
metadata = json.loads(metadata_json)
|
| 327 |
+
except json.JSONDecodeError as exc:
|
| 328 |
+
raise MangaColorizerError("Metadata invalida. Vuelve a extraer las imagenes.") from exc
|
| 329 |
+
original_paths = metadata.get("original_paths") or []
|
| 330 |
+
if not original_paths:
|
| 331 |
+
raise MangaColorizerError("No hay imagenes en cache.")
|
| 332 |
+
return metadata
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def get_colorized_path(original_path: str) -> Path:
|
| 336 |
+
source = Path(original_path)
|
| 337 |
+
colorized_dir = source.parent.parent / "colorized"
|
| 338 |
+
colorized_dir.mkdir(parents=True, exist_ok=True)
|
| 339 |
+
return colorized_dir / f"{source.stem}_colorized.png"
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def make_colorized_zip(colorized_paths: list[str]) -> str:
|
| 343 |
+
valid_paths = [Path(path) for path in colorized_paths if path and Path(path).exists()]
|
| 344 |
+
if not valid_paths:
|
| 345 |
+
raise MangaColorizerError("No hay imagenes coloreadas para comprimir.")
|
| 346 |
+
|
| 347 |
+
zip_path = valid_paths[0].parent / "colorized_manga.zip"
|
| 348 |
+
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zip_file:
|
| 349 |
+
for path in valid_paths:
|
| 350 |
+
zip_file.write(path, arcname=path.name)
|
| 351 |
+
return str(zip_path)
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def sample_extract_manga_image_urls_test() -> list[str]:
|
| 355 |
+
sample_html = """
|
| 356 |
+
<div id="1" style="height: 1280px;">
|
| 357 |
+
<img class="img-fluid no-lazy" src="https://hmanga.asia/wp-content/uploads/mangas/methonium-porque-eres-tu-enano/02.webp">
|
| 358 |
+
</div>
|
| 359 |
+
"""
|
| 360 |
+
return extract_manga_image_urls(sample_html, "https://hmanga.asia/porque-eres-tu-enano/#all")
|
requirements.txt
CHANGED
|
@@ -11,4 +11,6 @@ hf_xet
|
|
| 11 |
gradio
|
| 12 |
torch==2.11.0
|
| 13 |
numpy
|
| 14 |
-
av
|
|
|
|
|
|
|
|
|
| 11 |
gradio
|
| 12 |
torch==2.11.0
|
| 13 |
numpy
|
| 14 |
+
av
|
| 15 |
+
beautifulsoup4
|
| 16 |
+
requests
|