Update app.py
Browse files
app.py
CHANGED
|
@@ -356,372 +356,7 @@ def count_stats(text: str) -> dict:
|
|
| 356 |
'words': words,
|
| 357 |
}
|
| 358 |
|
| 359 |
-
|
| 360 |
-
# PDF生成
|
| 361 |
-
# ============================================================
|
| 362 |
-
|
| 363 |
-
def generate_pdf(src_orig: str, dst_orig: str, settings: dict,
|
| 364 |
-
src_html_body: str, dst_html_body: str, has_diff: bool) -> str:
|
| 365 |
-
|
| 366 |
-
FONT = "NotoSansJP"
|
| 367 |
-
PAGE_W, PAGE_H = A4
|
| 368 |
-
MARGIN = 15 * mm
|
| 369 |
-
COL_GAP = 4 * mm
|
| 370 |
-
COL_W = (PAGE_W - MARGIN * 2 - COL_GAP) / 2
|
| 371 |
-
FONT_SIZE = 8.5
|
| 372 |
-
LINE_HEIGHT= 13
|
| 373 |
-
COL_L = MARGIN
|
| 374 |
-
COL_R = MARGIN + COL_W + COL_GAP
|
| 375 |
-
DIV_X = MARGIN + COL_W + COL_GAP / 2
|
| 376 |
-
|
| 377 |
-
color_mode = settings.get('color_mode', 'color1')
|
| 378 |
-
if color_mode == 'color1':
|
| 379 |
-
HL_COLOR_RL = (1.0, 0.6, 0.6)
|
| 380 |
-
HL_FG_RL = (0.0, 0.0, 0.0)
|
| 381 |
-
HL_COLOR_MU = (1.0, 0.6, 0.6)
|
| 382 |
-
elif color_mode == 'color2':
|
| 383 |
-
HL_COLOR_RL = (0.6, 0.8, 1.0)
|
| 384 |
-
HL_FG_RL = (0.0, 0.0, 0.0)
|
| 385 |
-
HL_COLOR_MU = (0.6, 0.8, 1.0)
|
| 386 |
-
else: # monochrome
|
| 387 |
-
HL_COLOR_RL = (0.0, 0.0, 0.0)
|
| 388 |
-
HL_FG_RL = (1.0, 1.0, 1.0)
|
| 389 |
-
HL_COLOR_MU = (0.5, 0.5, 0.5)
|
| 390 |
-
|
| 391 |
-
# ----------------------------------------------------------
|
| 392 |
-
# HTMLから差分範囲を復元
|
| 393 |
-
# ----------------------------------------------------------
|
| 394 |
-
def extract_diff_ranges(text: str, html_body: str) -> list:
|
| 395 |
-
"""
|
| 396 |
-
html_bodyのspanタグからハイライト箇所の文字インデックスリストを復元する。
|
| 397 |
-
戻り値: [(start, end), ...] ※textのインデックス
|
| 398 |
-
"""
|
| 399 |
-
ranges = []
|
| 400 |
-
# ハイライトspanを検索
|
| 401 |
-
pattern = re.compile(
|
| 402 |
-
r'<span style="[^"]*background-color[^"]*">(.*?)</span>',
|
| 403 |
-
re.DOTALL
|
| 404 |
-
)
|
| 405 |
-
# タグを除去してプレーンテキストを再構築しながら位置を追跡
|
| 406 |
-
plain = ''
|
| 407 |
-
pos = 0
|
| 408 |
-
for m in re.finditer(
|
| 409 |
-
r'<span(?:\s+style="([^"]*)")?>(.*?)</span>',
|
| 410 |
-
html_body, re.DOTALL
|
| 411 |
-
):
|
| 412 |
-
style = m.group(1) or ''
|
| 413 |
-
content = m.group(2)
|
| 414 |
-
# <br>を改行に戻す
|
| 415 |
-
content = content.replace('<br>', '\n')
|
| 416 |
-
# HTMLエスケープを戻す
|
| 417 |
-
content = html.unescape(content)
|
| 418 |
-
is_hl = 'background-color' in style
|
| 419 |
-
start = len(plain)
|
| 420 |
-
plain += content
|
| 421 |
-
end = len(plain)
|
| 422 |
-
if is_hl:
|
| 423 |
-
ranges.append((start, end))
|
| 424 |
-
return ranges
|
| 425 |
-
|
| 426 |
-
src_diff_ranges = extract_diff_ranges(src_orig, src_html_body)
|
| 427 |
-
dst_diff_ranges = extract_diff_ranges(dst_orig, dst_html_body)
|
| 428 |
-
|
| 429 |
-
# ----------------------------------------------------------
|
| 430 |
-
# テキストを折り返し済み描画単位に変換
|
| 431 |
-
# ----------------------------------------------------------
|
| 432 |
-
def to_sublines(text: str, col_w: float, font_size: float):
|
| 433 |
-
"""
|
| 434 |
-
戻り値: [(char_offset, line_text), ...]
|
| 435 |
-
char_offset: テキスト先頭からの文字インデックス
|
| 436 |
-
"""
|
| 437 |
-
result = []
|
| 438 |
-
lines = text.split('\n')
|
| 439 |
-
offset = 0
|
| 440 |
-
for line in lines:
|
| 441 |
-
cur = ''
|
| 442 |
-
cur_start = offset
|
| 443 |
-
for ci, ch in enumerate(line):
|
| 444 |
-
test = cur + ch
|
| 445 |
-
if pdfmetrics.stringWidth(test, FONT, font_size) > col_w - 2 and cur:
|
| 446 |
-
result.append((cur_start, cur))
|
| 447 |
-
cur = ch
|
| 448 |
-
cur_start = offset + ci
|
| 449 |
-
else:
|
| 450 |
-
cur = test
|
| 451 |
-
result.append((cur_start, cur))
|
| 452 |
-
offset += len(line) + 1 # +1 for '\n'
|
| 453 |
-
return result
|
| 454 |
-
|
| 455 |
-
# ----------------------------------------------------------
|
| 456 |
-
# ハイライト判定
|
| 457 |
-
# ----------------------------------------------------------
|
| 458 |
-
def is_highlighted(char_idx: int, diff_ranges: list) -> bool:
|
| 459 |
-
for s, e in diff_ranges:
|
| 460 |
-
if s <= char_idx < e:
|
| 461 |
-
return True
|
| 462 |
-
return False
|
| 463 |
-
|
| 464 |
-
# ----------------------------------------------------------
|
| 465 |
-
# 1行描画(背景矩形 → 文字)
|
| 466 |
-
# 戻り値: ハイライトした文字の座標リスト [(x0,y0,x1,y1), ...]
|
| 467 |
-
# ※ PyMuPDF注釈付与用
|
| 468 |
-
# ----------------------------------------------------------
|
| 469 |
-
def draw_subline(c, sub_text: str, sub_offset: int, diff_ranges: list,
|
| 470 |
-
x_start: float, y_rl: float, font_size: float) -> list:
|
| 471 |
-
"""
|
| 472 |
-
y_rl: reportlabのy座標(下原点)
|
| 473 |
-
戻り値: ハイライト文字のrect座標リスト (reportlab座標系)
|
| 474 |
-
"""
|
| 475 |
-
if not sub_text:
|
| 476 |
-
return []
|
| 477 |
-
|
| 478 |
-
hl_rects = []
|
| 479 |
-
|
| 480 |
-
# パス1: 背景矩形
|
| 481 |
-
dx = x_start
|
| 482 |
-
for ci, ch in enumerate(sub_text):
|
| 483 |
-
cw = pdfmetrics.stringWidth(ch, FONT, font_size)
|
| 484 |
-
if is_highlighted(sub_offset + ci, diff_ranges):
|
| 485 |
-
c.saveState()
|
| 486 |
-
c.setFillColorRGB(*HL_COLOR_RL)
|
| 487 |
-
c.rect(dx, y_rl - 2, cw, LINE_HEIGHT - 1, fill=1, stroke=0)
|
| 488 |
-
c.restoreState()
|
| 489 |
-
hl_rects.append((dx, y_rl - 2, dx + cw, y_rl + LINE_HEIGHT - 1))
|
| 490 |
-
dx += cw
|
| 491 |
-
|
| 492 |
-
# パス2: 文字描画
|
| 493 |
-
dx = x_start
|
| 494 |
-
seg = ''
|
| 495 |
-
seg_hl = None
|
| 496 |
-
seg_x = x_start
|
| 497 |
-
|
| 498 |
-
for ci, ch in enumerate(sub_text):
|
| 499 |
-
hl = is_highlighted(sub_offset + ci, diff_ranges)
|
| 500 |
-
cw = pdfmetrics.stringWidth(ch, FONT, font_size)
|
| 501 |
-
if seg_hl is None:
|
| 502 |
-
seg_hl, seg, seg_x = hl, ch, dx
|
| 503 |
-
elif hl == seg_hl:
|
| 504 |
-
seg += ch
|
| 505 |
-
else:
|
| 506 |
-
c.saveState()
|
| 507 |
-
c.setFont(FONT, font_size)
|
| 508 |
-
c.setFillColorRGB(*(HL_FG_RL if seg_hl else (0.0, 0.0, 0.0)))
|
| 509 |
-
c.drawString(seg_x, y_rl, seg)
|
| 510 |
-
c.restoreState()
|
| 511 |
-
seg_x += pdfmetrics.stringWidth(seg, FONT, font_size)
|
| 512 |
-
seg_hl, seg = hl, ch
|
| 513 |
-
dx += cw
|
| 514 |
-
|
| 515 |
-
if seg:
|
| 516 |
-
c.saveState()
|
| 517 |
-
c.setFont(FONT, font_size)
|
| 518 |
-
c.setFillColorRGB(*(HL_FG_RL if seg_hl else (0.0, 0.0, 0.0)))
|
| 519 |
-
c.drawString(seg_x, y_rl, seg)
|
| 520 |
-
c.restoreState()
|
| 521 |
-
|
| 522 |
-
return hl_rects
|
| 523 |
-
|
| 524 |
-
# ----------------------------------------------------------
|
| 525 |
-
# reportlabでベースPDF生成
|
| 526 |
-
# ----------------------------------------------------------
|
| 527 |
-
def build_base_pdf() -> tuple:
|
| 528 |
-
"""
|
| 529 |
-
戻り値: (pdf_bytes, all_hl_rects)
|
| 530 |
-
all_hl_rects: {page_no: {'src': [(x0,y0_rl,x1,y1_rl),...], 'dst': [...]}}
|
| 531 |
-
※ y座標はreportlab座標系(下原点)
|
| 532 |
-
"""
|
| 533 |
-
buffer = io.BytesIO()
|
| 534 |
-
c = rl_canvas.Canvas(buffer, pagesize=A4)
|
| 535 |
-
all_hl = {} # page_no -> {'src':[], 'dst':[]}
|
| 536 |
-
page_no = 0
|
| 537 |
-
|
| 538 |
-
def hline(y, lw=0.5):
|
| 539 |
-
c.saveState()
|
| 540 |
-
c.setStrokeColorRGB(0.5, 0.5, 0.5)
|
| 541 |
-
c.setLineWidth(lw)
|
| 542 |
-
c.line(MARGIN, y, PAGE_W - MARGIN, y)
|
| 543 |
-
c.restoreState()
|
| 544 |
-
|
| 545 |
-
def new_page():
|
| 546 |
-
nonlocal page_no
|
| 547 |
-
c.showPage()
|
| 548 |
-
page_no += 1
|
| 549 |
-
|
| 550 |
-
def ensure_page_space(y, needed=LINE_HEIGHT):
|
| 551 |
-
if y < MARGIN + needed:
|
| 552 |
-
new_page()
|
| 553 |
-
return PAGE_H - MARGIN
|
| 554 |
-
return y
|
| 555 |
-
|
| 556 |
-
y = PAGE_H - MARGIN
|
| 557 |
-
|
| 558 |
-
# タイトル
|
| 559 |
-
c.saveState()
|
| 560 |
-
c.setFont(FONT, 18)
|
| 561 |
-
c.setFillColorRGB(0, 0, 0)
|
| 562 |
-
title = "テキスト比較ツール"
|
| 563 |
-
c.drawString((PAGE_W - pdfmetrics.stringWidth(title, FONT, 18)) / 2, y, title)
|
| 564 |
-
c.restoreState()
|
| 565 |
-
y -= 24
|
| 566 |
-
|
| 567 |
-
hline(y)
|
| 568 |
-
y -= 10
|
| 569 |
-
|
| 570 |
-
# 比較設定
|
| 571 |
-
c.saveState()
|
| 572 |
-
c.setFont(FONT, 10)
|
| 573 |
-
c.setFillColorRGB(0, 0, 0)
|
| 574 |
-
c.drawString(MARGIN, y, "■ 比較設定")
|
| 575 |
-
c.restoreState()
|
| 576 |
-
y -= 14
|
| 577 |
-
|
| 578 |
-
ignore_labels = {
|
| 579 |
-
'ignore_alpha': '英字(全半角)無視',
|
| 580 |
-
'ignore_digit': '数字(全半角)無視',
|
| 581 |
-
'ignore_kana': 'カナ(全半角)無視',
|
| 582 |
-
'ignore_yakumono': '約物(全半角)無視',
|
| 583 |
-
'ignore_ligature': 'リガチャ(合字)無視',
|
| 584 |
-
'ignore_kangxi': '康煕部首(CJK部首補助)無視',
|
| 585 |
-
'ignore_brackets_with_content': '括弧付き文字無視',
|
| 586 |
-
'ignore_kanjinum': '漢数字(算用数字)無視',
|
| 587 |
-
'ignore_hyphen': 'ハイフン無視',
|
| 588 |
-
'ignore_kumimoji': '組み文字無視',
|
| 589 |
-
'ignore_space': 'スペース(空白)削除',
|
| 590 |
-
'ignore_newline': '改行削除',
|
| 591 |
-
}
|
| 592 |
-
checked = [v for k, v in ignore_labels.items() if settings.get(k)]
|
| 593 |
-
c.saveState()
|
| 594 |
-
c.setFont(FONT, 8)
|
| 595 |
-
c.setFillColorRGB(0, 0, 0)
|
| 596 |
-
c.drawString(MARGIN, y, "有効な無視設定: " + ("、".join(checked) if checked else "なし"))
|
| 597 |
-
c.restoreState()
|
| 598 |
-
y -= 12
|
| 599 |
-
|
| 600 |
-
hline(y)
|
| 601 |
-
y -= 14
|
| 602 |
-
|
| 603 |
-
# 結果ヘッダー
|
| 604 |
-
c.saveState()
|
| 605 |
-
c.setFont(FONT, 14)
|
| 606 |
-
c.setFillColorRGB(0.85, 0, 0)
|
| 607 |
-
c.drawString(MARGIN, y, "相違が見つかりました" if has_diff else "相違はありません")
|
| 608 |
-
c.restoreState()
|
| 609 |
-
y -= 20
|
| 610 |
-
|
| 611 |
-
# カラムヘッダー
|
| 612 |
-
c.saveState()
|
| 613 |
-
c.setFont(FONT, 9)
|
| 614 |
-
c.setFillColorRGB(0, 0, 0)
|
| 615 |
-
c.drawString(COL_L, y, "【比較元】")
|
| 616 |
-
c.drawString(COL_R, y, "【比較先】")
|
| 617 |
-
c.restoreState()
|
| 618 |
-
y -= 14
|
| 619 |
-
|
| 620 |
-
# 本文描画
|
| 621 |
-
sublines_src = to_sublines(src_orig, COL_W, FONT_SIZE)
|
| 622 |
-
sublines_dst = to_sublines(dst_orig, COL_W, FONT_SIZE)
|
| 623 |
-
max_lines = max(len(sublines_src), len(sublines_dst))
|
| 624 |
-
while len(sublines_src) < max_lines:
|
| 625 |
-
sublines_src.append((len(src_orig), ''))
|
| 626 |
-
while len(sublines_dst) < max_lines:
|
| 627 |
-
sublines_dst.append((len(dst_orig), ''))
|
| 628 |
-
|
| 629 |
-
seg_y_top = y
|
| 630 |
-
all_hl[page_no] = {'src': [], 'dst': []}
|
| 631 |
-
|
| 632 |
-
for (off_s, txt_s), (off_d, txt_d) in zip(sublines_src, sublines_dst):
|
| 633 |
-
y = ensure_page_space(y)
|
| 634 |
-
if y == PAGE_H - MARGIN:
|
| 635 |
-
# ページ送り発生 → 縦区切り線を前ページに描画済み想定
|
| 636 |
-
# 新ページの縦線開始位置を更新
|
| 637 |
-
seg_y_top = y
|
| 638 |
-
all_hl[page_no] = {'src': [], 'dst': []}
|
| 639 |
-
|
| 640 |
-
rects_s = draw_subline(c, txt_s, off_s, src_diff_ranges, COL_L, y, FONT_SIZE)
|
| 641 |
-
rects_d = draw_subline(c, txt_d, off_d, dst_diff_ranges, COL_R, y, FONT_SIZE)
|
| 642 |
-
all_hl[page_no]['src'].extend(rects_s)
|
| 643 |
-
all_hl[page_no]['dst'].extend(rects_d)
|
| 644 |
-
y -= LINE_HEIGHT
|
| 645 |
-
|
| 646 |
-
# 縦区切り線
|
| 647 |
-
c.saveState()
|
| 648 |
-
c.setStrokeColorRGB(0.5, 0.5, 0.5)
|
| 649 |
-
c.setLineWidth(0.5)
|
| 650 |
-
c.line(DIV_X, seg_y_top, DIV_X, y + LINE_HEIGHT)
|
| 651 |
-
c.restoreState()
|
| 652 |
-
|
| 653 |
-
y -= 6
|
| 654 |
-
|
| 655 |
-
# 統計
|
| 656 |
-
hline(y, lw=0.3)
|
| 657 |
-
y -= 10
|
| 658 |
-
|
| 659 |
-
src_stats = count_stats(src_orig)
|
| 660 |
-
dst_stats = count_stats(dst_orig)
|
| 661 |
-
|
| 662 |
-
def draw_stats(x, stats):
|
| 663 |
-
nonlocal y
|
| 664 |
-
for line in [
|
| 665 |
-
f"文字数: {stats['char']}",
|
| 666 |
-
f"空白数: {stats['space']} 空白込み文字数: {stats['space_incl']}",
|
| 667 |
-
f"改行数: {stats['newline']} 改行込み文字数: {stats['newline_incl']}",
|
| 668 |
-
f"単語数: {stats['words']}",
|
| 669 |
-
]:
|
| 670 |
-
c.saveState()
|
| 671 |
-
c.setFont(FONT, 7.5)
|
| 672 |
-
c.setFillColorRGB(0.2, 0.2, 0.2)
|
| 673 |
-
c.drawString(x, y, line)
|
| 674 |
-
c.restoreState()
|
| 675 |
-
y -= 10
|
| 676 |
-
|
| 677 |
-
y_stats = y
|
| 678 |
-
draw_stats(COL_L, src_stats)
|
| 679 |
-
y = y_stats
|
| 680 |
-
draw_stats(COL_R, dst_stats)
|
| 681 |
-
|
| 682 |
-
c.save()
|
| 683 |
-
return buffer.getvalue(), all_hl
|
| 684 |
-
|
| 685 |
-
# ----------------------------------------------------------
|
| 686 |
-
# PyMuPDFでHighlight Annotation付与
|
| 687 |
-
# ----------------------------------------------------------
|
| 688 |
-
def add_highlight_annots(pdf_bytes: bytes, all_hl: dict) -> bytes:
|
| 689 |
-
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 690 |
-
|
| 691 |
-
for page_no, hl_data in all_hl.items():
|
| 692 |
-
if page_no >= len(doc):
|
| 693 |
-
continue
|
| 694 |
-
page = doc[page_no]
|
| 695 |
-
page_h = page.rect.height # PyMuPDF座標系の高さ(上原点)
|
| 696 |
-
|
| 697 |
-
rects_all = hl_data.get('src', []) + hl_data.get('dst', [])
|
| 698 |
-
for (x0, y0_rl, x1, y1_rl) in rects_all:
|
| 699 |
-
# reportlab座標(下原点) → PyMuPDF座標(上原点)変換
|
| 700 |
-
# reportlab: y0_rl=下端, y1_rl=上端
|
| 701 |
-
# PyMuPDF: y_top = page_h - y1_rl, y_bottom = page_h - y0_rl
|
| 702 |
-
mu_y_top = page_h - y1_rl
|
| 703 |
-
mu_y_bottom = page_h - y0_rl
|
| 704 |
-
rect = fitz.Rect(x0, mu_y_top, x1, mu_y_bottom)
|
| 705 |
-
annot = page.add_highlight_annot(rect)
|
| 706 |
-
annot.set_colors(stroke=HL_COLOR_MU)
|
| 707 |
-
annot.update()
|
| 708 |
-
|
| 709 |
-
result = io.BytesIO()
|
| 710 |
-
doc.save(result)
|
| 711 |
-
doc.close()
|
| 712 |
-
return result.getvalue()
|
| 713 |
-
|
| 714 |
-
# ----------------------------------------------------------
|
| 715 |
-
# 実行
|
| 716 |
-
# ----------------------------------------------------------
|
| 717 |
-
base_pdf_bytes, all_hl_rects = build_base_pdf()
|
| 718 |
-
final_pdf_bytes = add_highlight_annots(base_pdf_bytes, all_hl_rects)
|
| 719 |
-
|
| 720 |
-
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
|
| 721 |
-
tmp.write(final_pdf_bytes)
|
| 722 |
-
tmp.close()
|
| 723 |
-
return tmp.name
|
| 724 |
-
|
| 725 |
# ============================================================
|
| 726 |
# Gradio メイン処理
|
| 727 |
# ============================================================
|
|
|
|
| 356 |
'words': words,
|
| 357 |
}
|
| 358 |
|
| 359 |
+
undefined
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
# ============================================================
|
| 361 |
# Gradio メイン処理
|
| 362 |
# ============================================================
|