# -*- coding: utf-8 -*-
"""AI 文字生圖入門教學 — 18 頁簡報 (python-pptx)"""
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn

# ---------- palette ----------
BG_DARK   = "1D1636"
BG_DARK2  = "2A2150"
PRIMARY   = "6D28D9"
PRIMARY_D = "4C1D95"
TINT      = "F3EFFC"
TINT2     = "E4DBF7"
ACCENT    = "F59E0B"
ACCENT_T  = "FDF1DC"
ACCENT_D  = "92400E"
TEXT      = "23203A"
MUTED     = "6E6A85"
WHITE     = "FFFFFF"
LINE_C    = "DDD4F0"

FONT = "微软雅黑"
FONT_EN = "Segoe UI"
MONO = "Consolas"

prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
BLANK = prs.slide_layouts[6]
SW, SH = 13.333, 7.5

def C(hexs): return RGBColor.from_string(hexs)

def _set_ea(run, name):
    rPr = run._r.get_or_add_rPr()
    ea = rPr.find(qn('a:ea'))
    if ea is None:
        ea = rPr.makeelement(qn('a:ea'), {})
        latin = rPr.find(qn('a:latin'))
        if latin is not None:
            latin.addnext(ea)
        else:
            rPr.append(ea)
    ea.set('typeface', name)

def style_run(run, size=14, color=TEXT, bold=False, font=FONT, italic=False):
    f = run.font
    f.name = font; f.size = Pt(size); f.bold = bold; f.italic = italic
    f.color.rgb = C(color)
    _set_ea(run, FONT if font == FONT else FONT)

def add_box(slide, x, y, w, h, wrap=True):
    tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
    tf = tb.text_frame
    tf.word_wrap = wrap
    tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
    return tf

def para(tf, runs, size=14, color=TEXT, bold=False, align=PP_ALIGN.LEFT,
         before=0, after=0, leading=1.12, font=FONT, first=False):
    p = tf.paragraphs[0] if (first and not tf.paragraphs[0].runs) else tf.add_paragraph()
    p.alignment = align
    p.space_before = Pt(before); p.space_after = Pt(after)
    p.line_spacing = leading
    if isinstance(runs, str):
        runs = [(runs, {})]
    for txt, ov in runs:
        r = p.add_run(); r.text = txt
        style_run(r, size=ov.get("size", size), color=ov.get("color", color),
                  bold=ov.get("bold", bold), font=ov.get("font", font))
    return p

def rect(slide, x, y, w, h, fill=TINT, line=None, line_w=0.75, radius=0.10, shape=MSO_SHAPE.ROUNDED_RECTANGLE):
    sp = slide.shapes.add_shape(shape, Inches(x), Inches(y), Inches(w), Inches(h))
    if shape == MSO_SHAPE.ROUNDED_RECTANGLE:
        try: sp.adjustments[0] = radius
        except Exception: pass
    if fill is None:
        sp.fill.background()
    else:
        sp.fill.solid(); sp.fill.fore_color.rgb = C(fill)
    if line is None:
        sp.line.fill.background()
    else:
        sp.line.color.rgb = C(line); sp.line.width = Pt(line_w)
    sp.shadow.inherit = False
    return sp

def shape_text(sp, runs, size=14, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, leading=1.05, font=FONT):
    tf = sp.text_frame
    tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.05)
    tf.margin_top = tf.margin_bottom = Inches(0.02)
    tf.vertical_anchor = anchor
    para(tf, runs, size=size, color=color, bold=bold, align=align, leading=leading, first=True, font=font)
    return tf

def num_badge(slide, x, y, n, d=0.36, fill=PRIMARY, size=14):
    sp = rect(slide, x, y, d, d, fill=fill, radius=0.5)
    shape_text(sp, str(n), size=size, color=WHITE, bold=True)
    return sp

def bg(slide, color=WHITE):
    slide.background.fill.solid()
    slide.background.fill.fore_color.rgb = C(color)

def header(slide, kicker, title, page, dark=False):
    kc = ACCENT if dark else PRIMARY
    tc = WHITE if dark else TEXT
    tf = add_box(slide, 0.6, 0.42, 11.0, 0.32)
    para(tf, kicker, size=13, color=kc, bold=True, first=True)
    tf2 = add_box(slide, 0.6, 0.74, 11.6, 0.62)
    para(tf2, title, size=30, color=tc, bold=True, first=True)
    tf3 = add_box(slide, 12.35, 0.45, 0.55, 0.4)
    para(tf3, f"{page:02d}", size=14, color=(MUTED if not dark else "8B84B0"), bold=True,
         align=PP_ALIGN.RIGHT, first=True)

def footer(slide, page, dark=False):
    tf = add_box(slide, 0.6, 7.08, 5.0, 0.3)
    para(tf, "AI 文字生圖入門教學", size=12, color=("8B84B0" if dark else "B7B2C9"), first=True)
    tf2 = add_box(slide, 12.2, 7.08, 0.7, 0.3)
    para(tf2, f"{page:02d} / 18", size=12, color=("8B84B0" if dark else "B7B2C9"),
         align=PP_ALIGN.RIGHT, first=True)

def arrow_right(slide, x, y, w=0.34, h=0.22, color=TINT2):
    sp = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, Inches(x), Inches(y), Inches(w), Inches(h))
    sp.fill.solid(); sp.fill.fore_color.rgb = C(color)
    sp.line.fill.background(); sp.shadow.inherit = False
    return sp

def notes(slide, text):
    slide.notes_slide.notes_text_frame.text = text

# ============================================================
# S1 封面
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s, BG_DARK)
s.shapes.add_picture(r"C:\laragon\www\worker-system\ai-image-ppt\cover_grad.jpg",
                     Inches(8.35), Inches(0), Inches(4.983), Inches(7.5))
# prompt chip on image
chip = rect(s, 8.75, 6.55, 4.2, 0.52, fill=BG_DARK, radius=0.35)
shape_text(chip, [("prompt: ", {"color": ACCENT, "font": FONT_EN}),
                  ("深紫夜色 × 琥珀光暈，電影感", {"color": WHITE})],
           size=12, bold=False, align=PP_ALIGN.CENTER)
# left text
tf = add_box(s, 0.75, 2.02, 7.1, 0.4)
para(tf, "零基礎實戰工作坊", size=15, color=ACCENT, bold=True, first=True)
tf = add_box(s, 0.75, 2.5, 7.3, 1.9)
para(tf, "AI 文字生圖", size=58, color=WHITE, bold=True, first=True, leading=1.0)
para(tf, "入門教學", size=58, color=WHITE, bold=True, leading=1.0)
tf = add_box(s, 0.75, 4.75, 7.0, 1.1)
para(tf, "由提詞技巧、反提詞、JSON 檔案，", size=17, color="C9C2E8", first=True, leading=1.3)
para(tf, "到豆包與 Canva 影像工具實戰", size=17, color="C9C2E8", leading=1.3)
tf = add_box(s, 0.75, 6.35, 7.0, 0.4)
para(tf, "2026 年 8 月 · 教學簡報", size=13, color="8B84B0", first=True)
notes(s, "歡迎大家。今日呢個工作坊會由零開始教 AI 生圖：點寫提詞、點用一張圖反推提詞、JSON 提詞法，仲有豆包同 Canva 嘅實戰工具。")

# ============================================================
# S2 目錄
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "AGENDA", "今日學咩？", 2)
items = [
    ("01", "AI 生圖係咩", "一識就識用嘅新技能"),
    ("02", "提詞撰寫技巧", "萬能公式 + 風格詞庫"),
    ("03", "圖片反提詞", "見到靚圖，推返個 prompt 出嚟"),
    ("04", "JSON 提詞法", "結構化存檔，角色一致性"),
    ("05", "豆包影像工具", "擦除、重繪、擴圖一條龍"),
    ("06", "Canva 同工具比較", "排版、去水印、版權注意"),
]
for i, (n, t, d) in enumerate(items):
    col, row = i % 2, i // 2
    x = 0.6 + col * 6.25
    y = 1.85 + row * 1.72
    tf = add_box(s, x, y, 1.1, 0.9)
    para(tf, n, size=34, color=TINT2, bold=True, first=True, font=FONT_EN)
    tf = add_box(s, x + 1.05, y + 0.02, 4.7, 1.3)
    para(tf, t, size=19, color=TEXT, bold=True, first=True)
    para(tf, d, size=13.5, color=MUTED, before=4)
    if row < 2:
        rect(s, x, y + 1.42, 5.7, 0.012, fill=LINE_C, radius=0, shape=MSO_SHAPE.RECTANGLE)
footer(s, 2)
notes(s, "六個部分，頭四個講核心技巧，後面兩個係工具實戰同埋版權注意事項。")

# ============================================================
# S3 AI 生圖係咩
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "INTRODUCTION", "AI 生圖係咩？", 3)
boxes = [
    ("你嘅文字提詞", "用文字形容你想要嘅畫面", PRIMARY, WHITE),
    ("AI 擴散模型", "理解語意，從雜訊中「畫」出圖像", PRIMARY_D, WHITE),
    ("成品圖片", "幾秒一張，唔滿意就再試", ACCENT, WHITE),
]
bw, gap = 3.45, 0.62
x0 = (SW - (bw * 3 + gap * 2)) / 2
for i, (t, d, fc, tc) in enumerate(boxes):
    x = x0 + i * (bw + gap)
    sp = rect(s, x, 2.0, bw, 1.85, fill=fc, radius=0.10)
    tf = sp.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.25)
    tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    para(tf, t, size=20, color=tc, bold=True, align=PP_ALIGN.CENTER, first=True)
    para(tf, d, size=13.5, color=tc, align=PP_ALIGN.CENTER, before=6, leading=1.2)
    if i < 2:
        arrow_right(s, x + bw + 0.14, 2.78, color=TINT2)
facts = [
    ("唔使識畫畫", "識形容就得，人人用得到"),
    ("免費工具好好用", "豆包、即夢 AI 都有免費額度"),
    ("速度以秒計", "由諗法到成品，一分鐘內"),
]
for i, (t, d) in enumerate(facts):
    x = x0 + i * (bw + gap)
    tf = add_box(s, x, 4.55, bw, 1.5)
    para(tf, t, size=17, color=TEXT, bold=True, first=True)
    para(tf, d, size=13.5, color=MUTED, before=4, leading=1.2)
band = rect(s, 0.6, 6.1, 12.13, 0.66, fill=TINT, radius=0.12)
shape_text(band, [("一句到明：", {"color": PRIMARY}), ("你負責「講」，AI 負責「畫」——而講得好唔好，就決定幅圖靚唔靚。", {"color": TEXT})],
           size=15, bold=False)
footer(s, 3)
notes(s, "AI 生圖嘅原理係擴散模型，但教初學者唔使講太深，重點係：文字提詞就係你唯一嘅畫筆。")

# ============================================================
# S4 提詞萬能公式
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "PROMPT WRITING", "提詞萬能公式", 4)
parts = [
    ("主體", "橘色虎斑貓"),
    ("動作姿勢", "坐喺窗邊"),
    ("場景環境", "日式房間"),
    ("風格", "吉卜力動畫"),
    ("構圖鏡頭", "特寫鏡頭"),
    ("光線", "黃昏暖光"),
    ("畫質", "8K 高清"),
]
bw = 1.62; gap = 0.11
x0 = (SW - (bw * 7 + gap * 6)) / 2
for i, (t, ex) in enumerate(parts):
    x = x0 + i * (bw + gap)
    sp = rect(s, x, 2.15, bw, 0.66, fill=(PRIMARY if i in (0, 3) else TINT2 if False else PRIMARY_D if False else PRIMARY if False else (PRIMARY if i % 2 == 0 else PRIMARY_D)), radius=0.14)
    shape_text(sp, t, size=14.5, color=WHITE)
    tf = add_box(s, x, 2.98, bw, 0.5)
    para(tf, ex, size=12, color=MUTED, align=PP_ALIGN.CENTER, first=True)
    if i < 6:
        tf = add_box(s, x + bw - 0.03, 2.22, 0.2, 0.5)
        para(tf, "+", size=16, color=ACCENT, bold=True, align=PP_ALIGN.CENTER, first=True, font=FONT_EN)
band = rect(s, 0.9, 3.9, 11.53, 1.05, fill=TINT, radius=0.1)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.3); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "完整例子", size=12.5, color=PRIMARY, bold=True, first=True)
para(tf, "一隻橘色虎斑貓，坐喺窗邊望向窗外，日式房間，吉卜力動畫風格，特寫鏡頭，黃昏暖光，8K 高清",
     size=15.5, color=TEXT, before=3)
tips = [
    ("越具體越好", "數量、顏色、材質寫清楚：三隻暗紅色金屬杯，好過幾隻杯"),
    ("一句唔得就迭代", "生成 → 改一句 → 再生成，慢慢逼近心目中嘅畫面"),
]
for i, (t, d) in enumerate(tips):
    x = 0.9 + i * 5.95
    tf = add_box(s, x, 5.35, 5.5, 1.3)
    para(tf, t, size=16, color=PRIMARY, bold=True, first=True)
    para(tf, d, size=13.5, color=TEXT, before=4, leading=1.25)
footer(s, 4)
notes(s, "呢條公式係成堂課嘅核心。叫學員抄低：主體、動作、場景、風格、鏡頭、光線、畫質，七樣齊就至少有 80 分。")

# ============================================================
# S5 差 vs 好
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "PROMPT WRITING", "同一個諗法，兩個結果", 5)
# left: bad
lc = rect(s, 0.7, 1.95, 5.75, 4.3, fill="F8F7FB", line=LINE_C, radius=0.08)
badge = rect(s, 1.05, 2.3, 0.9, 0.42, fill="B7B2C9", radius=0.5)
shape_text(badge, "太籠統", size=13, color=WHITE)
tf = add_box(s, 1.05, 3.0, 5.0, 0.9)
para(tf, "「一隻貓」", size=26, color=TEXT, bold=True, first=True)
tf = add_box(s, 1.05, 4.0, 5.05, 1.9)
para(tf, "AI 只可以自由發揮：品種、風格、場景全部隨機。", size=14.5, color=MUTED, first=True, leading=1.3)
para(tf, "出嚟可能係卡通貓、黑白相、甚至七彩霓虹貓——好睇彩數。", size=14.5, color=MUTED, before=6, leading=1.3)
# right: good
rc = rect(s, 6.88, 1.95, 5.75, 4.3, fill=TINT, line=PRIMARY, line_w=1.2, radius=0.08)
badge = rect(s, 7.23, 2.3, 0.9, 0.42, fill=PRIMARY, radius=0.5)
shape_text(badge, "夠具體", size=13, color=WHITE)
tf = add_box(s, 7.23, 3.0, 5.05, 1.6)
para(tf, "「一隻橘色虎斑貓，坐喺窗邊，吉卜力動畫風格，黃昏暖光，特寫鏡頭」", size=16, color=TEXT, bold=True, first=True, leading=1.25)
tf = add_box(s, 7.23, 4.85, 5.05, 1.2)
para(tf, "場景、風格、光線全部鎖死，第一張已經貼近你想像，", size=14.5, color=PRIMARY_D, first=True, leading=1.3)
para(tf, "之後只需微調。", size=14.5, color=PRIMARY_D, leading=1.3)
band = rect(s, 0.7, 6.45, 11.93, 0.55, fill=ACCENT_T, radius=0.12)
shape_text(band, [("口訣：", {"color": ACCENT_D, "bold": True}),
                  ("你唔講，AI 就幫你作主——每個留白位都係隨機數。", {"color": TEXT})], size=14, bold=False)
footer(s, 5)
notes(s, "呢頁可以即場示範：同一句「一隻貓」同完整版分別生成，對比會好震撼。")

# ============================================================
# S6 進階技巧
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "PROMPT WRITING", "三個進階技巧", 6)
rows = [
    ("1", "負面提詞（Negative Prompt）", "講明「唔要咩」：模糊、多餘手指、文字水印。Midjourney 用 --no 參數，其他工具多數有負面詞欄位。"),
    ("2", "風格詞庫", "將試過好用嘅風格詞存低，下次直接套用——唔使次次由零開始諗。"),
    ("3", "鏡頭語言", "廣角、俯拍、淺景深、85mm 人像鏡頭……攝影詞彙令構圖即刻專業。"),
]
for i, (n, t, d) in enumerate(rows):
    y = 1.9 + i * 1.06
    num_badge(s, 0.7, y + 0.04, n)
    tf = add_box(s, 1.3, y, 10.9, 0.95)
    para(tf, t, size=17, color=TEXT, bold=True, first=True)
    para(tf, d, size=13.5, color=MUTED, before=3, leading=1.2)
    if i < 2:
        rect(s, 0.7, y + 0.92, 11.5, 0.012, fill=LINE_C, radius=0, shape=MSO_SHAPE.RECTANGLE)
tf = add_box(s, 0.7, 5.25, 6.0, 0.4)
para(tf, "常用風格詞（抄低即刻用）：", size=14, color=PRIMARY, bold=True, first=True)
chips = ["水彩風", "賽博朋克", "3D 皮克斯風", "中國水墨", "膠片攝影", "等距視角", "極簡主義", "厚塗插畫"]
cx, cy = 0.7, 5.75
for ch in chips:
    w = 0.42 + len(ch) * 0.185
    if cx + w > 12.7:
        cx = 0.7; cy += 0.52
    sp = rect(s, cx, cy, w, 0.42, fill=TINT2, radius=0.5)
    shape_text(sp, ch, size=12.5, color=PRIMARY_D, bold=True)
    cx += w + 0.18
footer(s, 6)
notes(s, "負面提詞係解決 AI 病嘅第一招；風格詞庫建議學員開一個 notes 存住。")

# ============================================================
# S7 反提詞
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "IMAGE TO PROMPT", "反提詞：由一張圖推返個 prompt", 7)
steps = [
    ("上載圖片", "揀一張你鍾意嘅靚圖"),
    ("叫 AI 描述", "「詳細描述呢張圖，\n再寫一段生成用 prompt」"),
    ("取得候選 prompt", "AI 會寫低一段\n完整英文/中文提詞"),
    ("補返風格詞", "手動加返感覺層面\n嘅風格同構圖詞"),
]
bw = 2.72; gap = 0.42
x0 = 0.6
for i, (t, d) in enumerate(steps):
    x = x0 + i * (bw + gap)
    sp = rect(s, x, 2.0, bw, 1.95, fill=(PRIMARY if i == 3 else TINT), radius=0.1)
    tf = sp.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.18); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    tc = WHITE if i == 3 else PRIMARY_D
    para(tf, t, size=16, color=tc, bold=True, align=PP_ALIGN.CENTER, first=True)
    para(tf, d.replace("\n", " "), size=12.5, color=tc, align=PP_ALIGN.CENTER, before=4, leading=1.2)
    if i < 3:
        arrow_right(s, x + bw + 0.045, 2.85, color=TINT2)
tf = add_box(s, 0.6, 4.35, 12.0, 0.4)
para(tf, "邊啲工具做到？", size=15, color=PRIMARY, bold=True, first=True)
chips = ["豆包", "ChatGPT / Gemini", "Midjourney /describe", "即夢 AI"]
cx = 0.6
for ch in chips:
    w = 0.42 + len(ch) * 0.185
    sp = rect(s, cx, 4.82, w, 0.46, fill=TINT2, radius=0.5)
    shape_text(sp, ch, size=12.5, color=PRIMARY_D, bold=True)
    cx += w + 0.2
band = rect(s, 0.6, 5.75, 12.13, 0.85, fill=ACCENT_T, radius=0.1)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.3); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "貼士：反提詞通常漏咗「感覺」", size=13, color=ACCENT_D, bold=True, first=True)
para(tf, "AI 描述講得出「有咩」，未必講得出「電影感、孤寂感」——呢啲要自己補返。", size=13, color=TEXT, before=2)
footer(s, 7)
notes(s, "反提詞係學習寫 prompt 嘅捷徑：拆解靚圖點寫，等於臨摹。")

# ============================================================
# S8 JSON 提詞法概念
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "JSON PROMPTING", "JSON 提詞法：點解要咁做？", 8)
cards = [
    ("易修改", "改一個欄位就換效果，唔使成句重寫。換個表情、轉個場景，一目了然。"),
    ("夠一致", "同一個 JSON 每次都出同一個角色——做多張圖嘅系列必備。"),
    ("可重用", "存檔、傳俾同事、幾個 JSON 拆開再組合，變成你嘅模組庫。"),
]
cw = 3.8; gap = 0.35
x0 = (SW - (cw * 3 + gap * 2)) / 2
for i, (t, d) in enumerate(cards):
    x = x0 + i * (cw + gap)
    sp = rect(s, x, 2.0, cw, 2.6, fill=TINT, radius=0.08)
    tf = sp.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.28); tf.margin_top = Inches(0.3)
    tf.vertical_anchor = MSO_ANCHOR.TOP
    para(tf, t, size=20, color=PRIMARY, bold=True, first=True)
    para(tf, d, size=14, color=TEXT, before=8, leading=1.35)
# flow
flow = ["一大段文字 prompt", "搵 AI 幫手整理", "結構化 JSON"]
bw = 3.3
x0 = (SW - (bw * 3 + 1.0 * 2)) / 2
for i, t in enumerate(flow):
    x = x0 + i * (bw + 1.0)
    sp = rect(s, x, 5.15, bw, 0.72, fill=(PRIMARY if i == 2 else WHITE), line=(None if i == 2 else PRIMARY), line_w=1.0, radius=0.12)
    shape_text(sp, t, size=14.5, color=WHITE if i == 2 else PRIMARY_D)
    if i < 2:
        arrow_right(s, x + bw + 0.31, 5.42, color=TINT2)
tf = add_box(s, 0.6, 6.25, 12.13, 0.5)
para(tf, "最啱用喺：人物設定集、故事繪本、品牌插畫等需要「同一個世界觀」嘅系列作品。",
     size=14, color=MUTED, align=PP_ALIGN.CENTER, first=True)
footer(s, 8)
notes(s, "JSON 唔使識寫 code——直接叫 ChatGPT「幫我將呢個 prompt 整理成 JSON 提詞」就得。")

# ============================================================
# S9 JSON 範例
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "JSON PROMPTING", "JSON 範例：茶餐廳老闆娘", 9)
panel = rect(s, 0.6, 1.85, 7.35, 4.85, fill=BG_DARK, radius=0.05)
tf = panel.text_frame; tf.word_wrap = False
tf.margin_left = Inches(0.35); tf.margin_top = Inches(0.28)
json_lines = [
    '{',
    '  "subject": {',
    '    "character":  "年輕女茶餐廳老闆娘",',
    '    "appearance": "短髮、白圍裙、手持奶茶",',
    '    "expression": "親切微笑"',
    '  },',
    '  "scene": {',
    '    "location":   "香港茶餐廳內",',
    '    "background": "卡座、霓虹招牌",',
    '    "time":       "下午"',
    '  },',
    '  "style": {',
    '    "art_style": "香港電影感攝影",',
    '    "lighting":  "暖黃燈光",',
    '    "camera":    "35mm 廣角、淺景深",',
    '    "quality":   "8K、高細節"',
    '  },',
    '  "negative": "模糊、變形手部、水印"',
    '}',
]
para(tf, "角色設定檔 · character.json", size=12, color="8B84B0", bold=True, first=True, font=MONO)
for ln in json_lines:
    key_color = ACCENT if ':' in ln and ln.strip().startswith('"') else ("A78BFA" if ln.strip().startswith('"') else "C9C2E8")
    para(tf, ln, size=12, color=key_color, font=MONO, leading=1.08)
notes_list = [
    ("subject", "主體：角色、外形、表情"),
    ("scene", "場景：地點、背景、時間"),
    ("style", "風格：畫風、光線、鏡頭、畫質"),
    ("negative", "負面詞：講明唔要咩"),
]
tf = add_box(s, 8.45, 2.0, 4.3, 3.2)
para(tf, "四個核心欄位", size=17, color=TEXT, bold=True, first=True)
for k, d in notes_list:
    para(tf, [(k, {"color": PRIMARY, "font": FONT_EN, "bold": True}), ("  " + d, {"color": MUTED})],
         size=13.5, before=10, leading=1.2)
band = rect(s, 8.45, 5.35, 4.3, 1.35, fill=TINT, radius=0.1)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.25); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "想改表情？", size=13, color=PRIMARY, bold=True, first=True)
para(tf, "只改一行：\"expression\": \"嚴肅\"", size=12.5, color=TEXT, before=3, font=MONO)
footer(s, 9)
notes(s, "呢個 JSON 可以逐欄位講：同中文長句比，結構化之後改嘢清晰好多。")

# ============================================================
# S10 JSON 還原流程
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "JSON PROMPTING", "JSON 還原：三步返去一張圖", 10)
steps = [
    ("貼入 AI", "將成個 JSON 貼落 ChatGPT 或豆包，叫佢「根據呢個 JSON 生成一張圖」或「先轉成自然語言再生成」。"),
    ("只改欄位", "改一行 JSON 就得：\n換表情、轉場景，唔使重寫成句。"),
    ("存做角色檔", "滿意之後存起個 JSON，以後呢個角色隨叫隨到，成個系列都一致。"),
]
bw = 3.75; gap = 0.45
x0 = (SW - (bw * 3 + gap * 2)) / 2
for i, (t, d) in enumerate(steps):
    x = x0 + i * (bw + gap)
    num_badge(s, x + 0.05, 1.95, i + 1, d=0.44, size=16)
    tf = add_box(s, x + 0.65, 1.98, bw - 0.7, 0.45)
    para(tf, t, size=18, color=TEXT, bold=True, first=True)
    sp = rect(s, x, 2.6, bw, 2.0, fill=TINT, radius=0.08)
    tf = sp.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.24); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    for k, seg in enumerate(d.split("\n")):
        para(tf, seg, size=13.5, color=TEXT, leading=1.35, first=(k == 0))
    if i < 2:
        arrow_right(s, x + bw + 0.05, 3.5, color=TINT2)
band = rect(s, 0.6, 5.15, 12.13, 1.45, fill=BG_DARK, radius=0.08)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.35); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "一個 JSON = 一個可重用嘅創作素材", size=16, color=ACCENT, bold=True, first=True)
para(tf, "文字 prompt 會愈改愈亂，JSON 就好似角色嘅「身分證」——改邊度、點改，清清楚楚。",
     size=14, color="C9C2E8", before=5)
footer(s, 10)
notes(s, "強調「還原」好簡單：JSON 只係一種寫法，貼返落任何識睇嘅 AI 就可以變返做圖。")

# ============================================================
# S11 豆包影像工具
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "DOUBAO TOOLS", "豆包：一個 App 搞掂生成同修圖", 11)
rows = [
    ("文生圖 / 圖生圖", "對話框打字就出圖；上載圖 + 打指令，即刻幫你改"),
    ("一鍵擦除（消除筆）", "塗抹唔要嘅路人雜物，AI 自動填返背景"),
    ("局部重繪", "塗一塗 + 打句指令：「將件衫改成紅色」"),
    ("AI 擴圖", "畫面唔夠闊，AI 自動補埋出邊嘅內容"),
    ("AI 抠圖・變清晰", "一鍵去背；舊相、模糊相提升解析度"),
]
for i, (t, d) in enumerate(rows):
    y = 1.9 + i * 0.99
    num_badge(s, 0.7, y + 0.06, i + 1, d=0.4)
    tf = add_box(s, 1.35, y, 6.9, 0.95)
    para(tf, t, size=16.5, color=TEXT, bold=True, first=True)
    para(tf, d, size=13.5, color=MUTED, before=3, leading=1.2)
    if i < 4:
        rect(s, 0.7, y + 0.86, 7.55, 0.012, fill=LINE_C, radius=0, shape=MSO_SHAPE.RECTANGLE)
card = rect(s, 8.75, 1.9, 4.0, 4.85, fill=BG_DARK, radius=0.08)
tf = card.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.35); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "Seedream 4.0", size=24, color=ACCENT, bold=True, first=True, font=FONT_EN)
para(tf, "豆包背後嘅圖像模型", size=13, color="8B84B0", before=3)
para(tf, "生成 + 編輯一體", size=16, color=WHITE, bold=True, before=18)
para(tf, "秒級出 2K 圖，支援 4K", size=14, color="C9C2E8", before=4)
para(tf, "一句話改圖，唔使識 PS", size=14, color="C9C2E8", before=4)
para(tf, "手機 App / 網頁版免費用", size=14, color="C9C2E8", before=4)
footer(s, 11)
notes(s, "豆包嘅優勢係「對話即工具」：唔使去唔同網站，傾偈一樣咁改圖。")

# ============================================================
# S12 Canva Magic Studio
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "CANVA MAGIC STUDIO", "Canva Magic Studio：排版前嘅魔法工具", 12)
feats = [
    ("Magic Eraser", "魔力橡皮擦：筆刷塗抹，刪走雜物路人"),
    ("Magic Edit", "塗抹 + 打字，AI 重繪嗰一忽位"),
    ("Magic Expand", "智能擴圖：直圖變橫圖，改長寬比"),
    ("Magic Grab", "將主體「抽」出嚟變獨立圖層，自由移動"),
    ("Magic Media", "喺 Canva 入面直接文生圖、文生片"),
    ("背景移除", "一鍵去背，出 transparent PNG"),
]
for i, (t, d) in enumerate(feats):
    col, row = i % 2, i // 2
    x = 0.6 + col * 6.25
    y = 1.95 + row * 1.18
    num_badge(s, x, y + 0.05, "", d=0.4) if False else None
    sp = rect(s, x, y + 0.02, 0.14, 0.14, fill=PRIMARY, radius=0.5, shape=MSO_SHAPE.OVAL)
    tf = add_box(s, x + 0.32, y, 5.6, 1.05)
    para(tf, t, size=15.5, color=PRIMARY_D, bold=True, first=True, font=FONT_EN)
    para(tf, d, size=13, color=TEXT, before=2, leading=1.2)
band = rect(s, 0.6, 5.55, 7.4, 1.1, fill=TINT, radius=0.1)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.3); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "組合技", size=13, color=PRIMARY, bold=True, first=True)
para(tf, "Expand 解決「空間唔夠」＋ Grab 解決「主體位唔靚」＝簡易排版流程。", size=13.5, color=TEXT, before=2)
warn = rect(s, 8.25, 5.55, 4.48, 1.1, fill=ACCENT_T, radius=0.1)
tf = warn.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.28); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "留意：Eraser / Expand 等", size=13, color=ACCENT_D, bold=True, first=True)
para(tf, "多數要 Canva Pro 先用得盡，免費版有限次。", size=13, color=TEXT, before=2)
footer(s, 12)
notes(s, "Canva 嘅強項係生成之後直接排版——出 IG post、海報一條龍。提一提免費版限制。")

# ============================================================
# S13 去水印 + 版權
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "WATERMARK & ETHICS", "去水印：先講規矩，再講方法", 13)
warn = rect(s, 0.6, 1.85, 12.13, 1.15, fill="FBEFEF", radius=0.08)
tf = warn.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.35); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "版權警告", size=15, color="B91C1C", bold=True, first=True)
para(tf, "只可以用喺自己生成、或者獲授權嘅圖片。去除他人作品嘅水印再商用，隨時構成侵權。",
     size=14, color=TEXT, before=3)
methods = [
    ("①", "正版渠道（最穩陣）", "AI 工具自己加嘅水印，多數付費升級就出無水印版——豆包、即夢都係咁。"),
    ("②", "AI 擦除", "用豆包消除筆 / Canva Magic Eraser 塗走水印位置，AI 自動填背景。"),
    ("③", "裁切（最快）", "水印喺角落就直接 crop 走，犧牲少少構圖。"),
]
for i, (n, t, d) in enumerate(methods):
    y = 3.35 + i * 1.12
    tf = add_box(s, 0.7, y, 0.6, 0.6)
    para(tf, n, size=22, color=PRIMARY, bold=True, first=True)
    tf = add_box(s, 1.45, y, 11.1, 1.0)
    para(tf, t, size=16, color=TEXT, bold=True, first=True)
    para(tf, d, size=13.5, color=MUTED, before=3, leading=1.2)
    if i < 2:
        rect(s, 0.7, y + 0.96, 11.9, 0.012, fill=LINE_C, radius=0, shape=MSO_SHAPE.RECTANGLE)
band = rect(s, 0.6, 6.5, 12.13, 0.55, fill=TINT, radius=0.12)
shape_text(band, [("原則：", {"color": PRIMARY, "bold": True}),
                  ("用自己嘅圖先好用呢啲方法；用其他嘅圖，尊重創作者。", {"color": TEXT})], size=14, bold=False)
footer(s, 13)
notes(s, "教去水印一定要同時教倫理。方法本身好簡單，重點係幾時可以、幾時唔可以。")

# ============================================================
# S14 工具比較表
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "TOOL COMPARISON", "主流工具點揀？", 14)
cols = [2.3, 4.7, 1.9, 3.2]
xs = [0.6]
for w in cols[:-1]:
    xs.append(xs[-1] + w)
heads = ["工具", "亮點", "費用", "適合邊個"]
rows = [
    ("豆包 / 即夢 AI", "中文 prompt 友好；生成+編輯+擦除一條龍", "免費", "初學者、日常用途"),
    ("Midjourney", "質感同藝術性最靚，社群強大", "收費", "追求畫質嘅創作者"),
    ("Canva", "生成之後直接排版出 post / 海報", "基礎免費", "要一條龍出成品嘅人"),
    ("Stable Diffusion", "本地運行、完全可控、無審查", "免費", "鍾意攪機、想深度自訂"),
]
hy = 1.95
for j, htxt in enumerate(heads):
    sp = rect(s, xs[j], hy, cols[j] - 0.06, 0.55, fill=PRIMARY, radius=0.08)
    shape_text(sp, htxt, size=14.5, color=WHITE, bold=True, align=PP_ALIGN.LEFT if j < 4 else PP_ALIGN.CENTER)
    sp.text_frame.margin_left = Inches(0.2)
for i, row in enumerate(rows):
    y = hy + 0.62 + i * 0.92
    fillc = TINT if i % 2 == 0 else WHITE
    for j, cell in enumerate(row):
        sp = rect(s, xs[j], y, cols[j] - 0.06, 0.86, fill=fillc,
                  line=(None if i % 2 == 0 else LINE_C), line_w=0.5, radius=0.05)
        bold = (j == 0)
        color = PRIMARY_D if j == 0 else (ACCENT_D if j == 2 and cell == "收費" else TEXT)
        shape_text(sp, cell, size=13.5, color=color, bold=bold, align=PP_ALIGN.LEFT)
        sp.text_frame.margin_left = Inches(0.2)
band = rect(s, 0.6, 6.35, 12.13, 0.6, fill=ACCENT_T, radius=0.12)
shape_text(band, [("建議起步：", {"color": ACCENT_D, "bold": True}),
                  ("豆包 / 即夢 練提詞 → 想再靚就上 Midjourney → 要出成品就用 Canva 排版。", {"color": TEXT})],
           size=14, bold=False)
footer(s, 14)
notes(s, "四隻工具各有定位，唔使全部學齊——初學者揀豆包/即夢入門最順。")

# ============================================================
# S15 常見失敗位
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "TROUBLESHOOTING", "常見 AI 失敗位同避法", 15)
rows = [
    ("手指變形、六隻手指", "負面詞加「變形手部」；畫遠景少畫手；唔滿意就重生成幾次"),
    ("文字亂碼（中文尤其）", "生圖唔好依賴 AI 寫字——出圖後用 Canva / 豆包自己加字"),
    ("風格走樣、前後唔一致", "用 JSON 提詞鎖住風格；或者用參考圖（圖生圖）"),
    ("畫面太似另一個作品", "換風格詞、換構圖；商用前檢查有冇抄襲嫌疑"),
]
for i, (t, d) in enumerate(rows):
    y = 1.9 + i * 1.02
    sp = rect(s, 0.7, y + 0.05, 0.16, 0.16, fill=ACCENT, radius=0.5, shape=MSO_SHAPE.OVAL)
    tf = add_box(s, 1.1, y, 11.3, 0.95)
    para(tf, t, size=16, color=TEXT, bold=True, first=True)
    para(tf, [("解法：", {"color": PRIMARY, "bold": True}), (d, {"color": MUTED})],
         size=13.5, before=3, leading=1.2)
    if i < 3:
        rect(s, 0.7, y + 0.88, 11.9, 0.012, fill=LINE_C, radius=0, shape=MSO_SHAPE.RECTANGLE)
tf = add_box(s, 0.7, 6.0, 3.0, 0.4)
para(tf, "尺寸比例速查：", size=14, color=PRIMARY, bold=True, first=True)
chips = [("1:1", "IG 貼文"), ("9:16", "Reels / 限動"), ("16:9", "PPT / 影片"), ("3:4", "海報 / A4")]
cx = 0.7
for k, v in chips:
    w = 1.9
    sp = rect(s, cx, 6.45, w, 0.55, fill=TINT2, radius=0.15)
    tf = sp.text_frame; tf.word_wrap = False
    tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    tf.margin_left = tf.margin_right = Inches(0.05)
    para(tf, [(k, {"color": PRIMARY_D, "bold": True, "font": FONT_EN, "size": 14}),
              ("  " + v, {"color": MUTED, "size": 12.5})], align=PP_ALIGN.CENTER, first=True)
    cx += w + 0.22
footer(s, 15)
notes(s, "呢頁係「救火指南」：學員遇到問題對號入座。比例速查好實用，建議影相存起。")

# ============================================================
# S16 實戰工作流
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "WORKFLOW", "一條龍實戰：由 prompt 到 IG post", 16)
steps = [
    ("寫提詞", "用萬能公式\n寫好 prompt"),
    ("生成", "豆包 / 即夢\n出第一稿"),
    ("修圖", "擦除雜物\n局部重繪"),
    ("去水印", "正版下載\n或 AI 擦除"),
    ("排版", "Canva 加字\n出 IG post"),
]
bw = 2.24; gap = 0.28
x0 = (SW - (bw * 5 + gap * 4)) / 2
for i, (t, d) in enumerate(steps):
    x = x0 + i * (bw + gap)
    num_badge(s, x + bw / 2 - 0.22, 1.95, i + 1, d=0.44, size=16)
    sp = rect(s, x, 2.65, bw, 1.7, fill=(PRIMARY if i == 4 else TINT), radius=0.1)
    tf = sp.text_frame; tf.word_wrap = True
    tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    tc = WHITE if i == 4 else PRIMARY_D
    para(tf, t, size=17, color=tc, bold=True, align=PP_ALIGN.CENTER, first=True)
    para(tf, d.replace("\n", " "), size=12.5, color=tc, align=PP_ALIGN.CENTER, before=3, leading=1.2)
    if i < 4:
        arrow_right(s, x + bw + 0.005, 3.4, color=TINT2)
band = rect(s, 0.9, 5.0, 11.53, 1.35, fill=BG_DARK, radius=0.08)
tf = band.text_frame; tf.word_wrap = True
tf.margin_left = tf.margin_right = Inches(0.35); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
para(tf, "成條 flow 十分鐘內搞掂", size=16, color=ACCENT, bold=True, first=True)
para(tf, "重點唔係一張圖就perfect——係「快試快改」：每個步驟都有免費工具，試錯成本接近零。",
     size=14, color="C9C2E8", before=5)
footer(s, 16)
notes(s, "呢頁可以現場 demo 一條龍，或者俾學員分組跟住做一次。")

# ============================================================
# S17 版權與倫理
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s)
header(s, "ETHICS", "用得開心，都要用得放心", 17)
rules = [
    ("標明 AI 生成", "對外發布嘅圖，誠實標示係 AI 做嘅，唔好冒充實拍。"),
    ("唔好生成真人肖像", "未經同意用名人/朋友樣貌生成圖片，有法律同道德風險。"),
    ("商用前查清楚", "唔同工具嘅商用授權唔同；免費版可能限制商用。"),
    ("尊重他人作品", "唔好 remove 他人水印、抄襲風格仲商用。"),
]
for i, (t, d) in enumerate(rules):
    col, row = i % 2, i // 2
    x = 0.6 + col * 6.25
    y = 2.0 + row * 1.85
    sp = rect(s, x, y, 5.85, 1.55, fill=TINT, radius=0.08)
    tf = sp.text_frame; tf.word_wrap = True
    tf.margin_left = tf.margin_right = Inches(0.3); tf.vertical_anchor = MSO_ANCHOR.MIDDLE
    para(tf, t, size=16.5, color=PRIMARY_D, bold=True, first=True)
    para(tf, d, size=13, color=TEXT, before=4, leading=1.25)
band = rect(s, 0.6, 6.1, 12.13, 0.6, fill=ACCENT_T, radius=0.12)
shape_text(band, [("一句總結：", {"color": ACCENT_D, "bold": True}),
                  ("AI 係工具，責任喺用嘅人——透明、尊重、查清楚。", {"color": TEXT})], size=14, bold=False)
footer(s, 17)
notes(s, "倫理呢頁唔使長篇大論，四條規則帶到重點。")

# ============================================================
# S18 總結
# ============================================================
s = prs.slides.add_slide(BLANK)
bg(s, BG_DARK)
tf = add_box(s, 0.75, 1.35, 11.8, 0.4)
para(tf, "RECAP · 帶走呢一頁就夠", size=15, color=ACCENT, bold=True, first=True)
tf = add_box(s, 0.75, 1.95, 11.8, 1.9)
para(tf, "主體 + 動作 + 場景", size=40, color=WHITE, bold=True, first=True, leading=1.15)
para(tf, "+ 風格 + 鏡頭 + 光線", size=40, color=ACCENT, bold=True, leading=1.15)
tf = add_box(s, 0.75, 4.15, 11.8, 1.2)
para(tf, "由呢條公式開始，即刻試下寫你第一個 prompt。", size=19, color="C9C2E8", first=True, leading=1.3)
para(tf, "出圖唔滿意？改一句，再試——AI 生圖最平嘅係「再嚟一次」。", size=19, color="C9C2E8", leading=1.3)
rect(s, 0.75, 5.85, 11.8, 0.012, fill="3A2F63", radius=0, shape=MSO_SHAPE.RECTANGLE)
tf = add_box(s, 0.75, 6.15, 11.8, 0.5)
para(tf, "多謝收聽 · 一齊嚟生第一張圖啦！", size=17, color=WHITE, bold=True, first=True)
footer(s, 18, dark=True)
notes(s, "收結：叫學員即場用公式寫一個 prompt 生成第一張圖，完成正循環。")

# ---------- save ----------
prs.core_properties.title = "AI 文字生圖入門教學"
prs.core_properties.author = "ZCode"
out = r"C:\laragon\www\worker-system\ai-image-ppt\AI文字生圖入門教學.pptx"
prs.save(out)
print("saved:", out, "| slides:", len(prs.slides.__iter__.__self__._sldIdLst))
