#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
华腾养虾记（一）· 热场视频 60s
风格：偏活泼酷炫 —— 卡点硬切、drop 闪白、粒子/速度线、霓虹描边。
音乐 120BPM，所有切点落在小节线上（2s 一小节）。
帧数据直接 pipe 给 ffmpeg，不落 PNG。
"""
import os, math, subprocess, sys
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter

W, H, FPS = 1920, 1080, 30
DUR = 60.0
NF = int(DUR * FPS)
SW, SH = 480, 270                      # 背景低分辨率画布
HERE = os.path.dirname(os.path.abspath(__file__))
BOLD = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
REG = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
IDX = 2

BPM = 120.0
BEAT = 60.0 / BPM
BAR = 4 * BEAT

BLUE = (0, 56, 118)
ORANGE = (221, 106, 10)
ORG_L = (255, 150, 46)
CYAN = (0, 210, 255)
WHITE = (255, 255, 255)
DIM = (156, 180, 210)
DIM2 = (104, 130, 162)

_fc = {}
def F(sz, bold=True):
    k = (sz, bold)
    if k not in _fc:
        _fc[k] = ImageFont.truetype(BOLD if bold else REG, sz, index=IDX)
    return _fc[k]

CX = W // 2

# ---------------------------------------------------------------- 文本图层
def _probe():
    return ImageDraw.Draw(Image.new("RGBA", (8, 8)))

def txt(lines, size, color, bold=True, lh=1.42, glow=None, shadow=True):
    if isinstance(lines, str):
        lines = [lines]
    f = F(size, bold)
    p = _probe()
    wmax = max(p.textlength(l, font=f) for l in lines)
    lineh = int(size * lh)
    pad = int(size * 0.42) + 14
    im = Image.new("RGBA", (int(wmax) + pad * 2, lineh * len(lines) + pad * 2), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    for i, l in enumerate(lines):
        d.text((pad, pad + i * lineh), l, font=f, fill=color)
    if glow:
        a = im.getchannel("A").filter(ImageFilter.GaussianBlur(size * 0.20))
        g = Image.new("RGBA", im.size, glow + (0,))
        g.putalpha(a.point(lambda v: int(v * 0.75)))
        im = Image.alpha_composite(g, im)
    if shadow:
        sh = Image.new("RGBA", im.size, (0, 0, 0, 0))
        sd = ImageDraw.Draw(sh)
        for i, l in enumerate(lines):
            sd.text((pad + 3, pad + i * lineh + 5), l, font=f, fill=(0, 8, 20, 150))
        sh = sh.filter(ImageFilter.GaussianBlur(4))
        im = Image.alpha_composite(sh, im)
    return im

def rich(parts, size, bold=True, lh=1.42, glow=None):
    f = F(size, bold)
    p = _probe()
    tw = sum(p.textlength(t, font=f) for t, _ in parts)
    pad = int(size * 0.42) + 14
    im = Image.new("RGBA", (int(tw) + pad * 2, int(size * lh) + pad * 2), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    x = pad
    for t, c in parts:
        d.text((x, pad), t, font=f, fill=c)
        x += p.textlength(t, font=f)
    if glow:
        a = im.getchannel("A").filter(ImageFilter.GaussianBlur(size * 0.20))
        g = Image.new("RGBA", im.size, glow + (0,))
        g.putalpha(a.point(lambda v: int(v * 0.7)))
        im = Image.alpha_composite(g, im)
    sh = Image.new("RGBA", im.size, (0, 0, 0, 0))
    sd = ImageDraw.Draw(sh)
    x = pad
    for t, c in parts:
        sd.text((x + 3, pad + 5), t, font=f, fill=(0, 8, 20, 150))
        x += p.textlength(t, font=f)
    return Image.alpha_composite(sh.filter(ImageFilter.GaussianBlur(4)), im)

def pill(text, size=30, fg=CYAN):
    f = F(size, True)
    p = _probe()
    tw = p.textlength(text, font=f)
    dot, px, py = 12, 32, 18
    w, h = int(tw + px * 2 + dot + 14), int(size + py * 2)
    im = Image.new("RGBA", (w + 24, h + 24), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    d.rounded_rectangle([12, 12, w + 11, h + 11], radius=h // 2,
                        fill=fg + (30,), outline=fg + (150,), width=2)
    cy = 12 + h // 2
    d.ellipse([12 + px - 2, cy - dot // 2, 12 + px - 2 + dot, cy + dot // 2], fill=fg)
    d.text((12 + px + dot + 12, cy - size * 0.74), text, font=f, fill=fg)
    return im

def chip(title, sub, w, h, accent=CYAN, hot=False, num=None, tsz=48, ssz=27):
    im = Image.new("RGBA", (w + 30, h + 30), (0, 0, 0, 0))
    d = ImageDraw.Draw(im)
    box = [15, 15, w + 14, h + 14]
    d.rounded_rectangle(box, radius=20,
                        fill=(0, 96, 200, 120) if hot else (255, 255, 255, 18),
                        outline=accent + (185,) if hot else (255, 255, 255, 52), width=2)
    d.rounded_rectangle([15, 15, 21, h + 14], radius=3, fill=accent + (240,))
    x, y = 15 + 34, 15 + int(h * 0.5 - tsz * 0.92)
    if num:
        fn = F(25, True)
        d.ellipse([x, y + 4, x + 38, y + 42], fill=accent + (52,), outline=accent + (165,), width=2)
        tw = d.textlength(num, font=fn)
        d.text((x + 19 - tw / 2, y + 23 - 25 * 0.74), num, font=fn, fill=accent)
        x += 56
    d.text((x, y), title, font=F(tsz, True), fill=WHITE if hot else (230, 240, 250))
    yy = y + int(tsz * 1.30)
    for s in sub:
        d.text((x, yy), s, font=F(ssz, False), fill=(200, 226, 255) if hot else DIM)
        yy += int(ssz * 1.45)
    if hot:
        a = im.getchannel("A").filter(ImageFilter.GaussianBlur(14))
        g = Image.new("RGBA", im.size, accent + (0,))
        g.putalpha(a.point(lambda v: int(v * 0.34)))
        im = Image.alpha_composite(g, im)
    return im

# ---------------------------------------------------------------- logo
_LG = Image.open(os.path.join(HERE, "..", "huatech-logo.png")).convert("RGBA")
_LG = _LG.crop(_LG.getbbox())
def logo(h, glow=False):
    im = _LG.resize((round(_LG.width * h / _LG.height), h), Image.LANCZOS)
    px = im.load()
    for y in range(im.height):
        for x in range(im.width):
            r, g, b, a = px[x, y]
            if a:
                px[x, y] = (255, 255, 255, a)
    if glow:
        pad = 40
        c = Image.new("RGBA", (im.width + pad * 2, im.height + pad * 2), (0, 0, 0, 0))
        c.alpha_composite(im, (pad, pad))
        a = c.getchannel("A").filter(ImageFilter.GaussianBlur(20))
        g = Image.new("RGBA", c.size, CYAN + (0,))
        g.putalpha(a.point(lambda v: int(v * 0.55)))
        im = Image.alpha_composite(g, c)
    return im

LG_XL = logo(120, True)
LG_SM = logo(46)
LG_MD = logo(60, True)

# ---------------------------------------------------------------- 元素/场景
class El:
    __slots__ = ("img", "x", "y", "d", "dur", "rise", "kind")
    def __init__(self, img, x, y, d=0.0, dur=0.42, rise=30, anchor="ct", kind="up"):
        w = img.size[0]
        if anchor == "ct": x -= w // 2
        elif anchor == "rt": x -= w
        self.img, self.x, self.y = img, int(x), int(y)
        self.d, self.dur, self.rise, self.kind = d, dur, rise, kind

SC = []
def S(t0, t1, els):
    SC.append((t0, t1, els))

B = BEAT
# ---- 1 | 0-4s 开场（只有 pad，安静）
S(0.0, 4.0, [
    El(LG_XL, CX, 392, 0.15, 0.85, 34),
    El(txt("数字员工培养计划", 36, DIM, False, glow=None, shadow=False), CX, 578, 1.0, 0.6, 20),
    El(txt("第 一 期", 30, ORG_L, True, shadow=False), CX, 648, 1.5, 0.5, 16),
])
# ---- 2 | 4-8s 主标题
S(4.0, 8.0, [
    El(pill("今天下午 · 全员参加"), CX, 244, 0.0, 0.34, 22),
    El(rich([("华腾养虾记", WHITE), ("（一）", ORG_L)], 152, glow=(0, 90, 200)), CX, 344, B * 0.5, 0.5, 44, kind="pop"),
    El(rich([("打造属于", DIM), ("你自己", ORG_L), ("的数字员工", DIM)], 54), CX, 604, B * 1.5, 0.42, 26),
    El(txt("全员参加　|　无需提前准备", 32, DIM2, False, shadow=False), CX, 726, B * 2.5, 0.4, 18),
])
# ---- 3 | 8-12s 钩子
S(8.0, 12.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(txt("先想一个问题", 34, CYAN, False, shadow=False), CX, 268, 0.0, 0.34, 18),
    El(txt("你每天干的活里", 100, WHITE, glow=(0, 80, 190)), CX, 350, B * 0.5, 0.46, 38, kind="pop"),
    El(rich([("有多少是在干", WHITE), ("第二遍", ORG_L), ("？", WHITE)], 100, glow=(0, 80, 190)), CX, 502, B * 1.5, 0.46, 38, kind="pop"),
    El(txt("AI 打字快省不了多少时间 —— 不干第二遍才是真的省", 32, DIM2, False, shadow=False), CX, 740, B * 3, 0.4, 18),
])
# ---- 4 | 12-18s 三层（riser 14-18）
CW, CHH = 470, 210
S(12.0, 18.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(pill("第 1 章"), 128, 196, 0.0, 0.3, 14, "lt"),
    El(txt("先分清三层东西", 76, WHITE, glow=(0, 70, 170)), 118, 274, 0.14, 0.42, 26, "lt"),
    El(chip("大模型", ["会思考的大脑"], CW, CHH, DIM2, False, "1", 46, 27), 136, 486, B * 1.0, 0.4, 30, "lt"),
    El(chip("AI 工具", ["能问，不能干"], CW, CHH, CYAN, False, "2", 46, 27), 726, 486, B * 2.0, 0.4, 30, "lt"),
    El(chip("数字员工", ["能动手干活的同事"], CW, CHH, ORG_L, True, "3", 46, 27), 1316, 486, B * 3.0, 0.4, 30, "lt"),
    El(txt("有工具 · 有记忆 · 有权限　—　三者缺一，就退化成聊天机器人", 32, DIM2, False, shadow=False), CX, 800, B * 5, 0.4, 18),
])
# ---- 5 | 18-24s DROP
S(18.0, 24.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(rich([("问答型 AI 给你", DIM), ("答案", DIM)], 78), CX, 318, 0.05, 0.34, 30, kind="pop"),
    El(rich([("数字员工给你", WHITE), ("结果", ORG_L)], 128, glow=(0, 90, 200)), CX, 438, B * 1.0, 0.5, 42, kind="pop"),
    El(txt("32MB 的白皮书，几分钟拆完", 40, (206, 230, 255), False, shadow=False), CX, 672, B * 3.0, 0.42, 22),
    El(txt("给你一份带出处的问题清单", 40, CYAN, True, shadow=False), CX, 740, B * 4.0, 0.42, 22),
])
# ---- 6 | 24-32s 四类场景
BW, BHH = 800, 152
S(24.0, 32.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(pill("现场演示"), 128, 190, 0.0, 0.3, 14, "lt"),
    El(txt("四类真能用的场景", 74, WHITE, glow=(0, 70, 170)), 118, 266, 0.14, 0.42, 26, "lt"),
    El(chip("产品设计 · 文档分析", ["招标文件 → 结构问题、前后矛盾、缺失项"], BW, BHH, CYAN, True, None, 42, 26), 118, 452, B * 1, 0.36, 34, "lt"),
    El(chip("材料整理", ["会议纪要 → 事项规范；需求 → 用例清单"], BW, BHH, CYAN, False, None, 42, 26), 972, 452, B * 2, 0.36, 34, "lt"),
    El(chip("代码与系统理解", ["老模块 → 结构说明 + 改造方案 + 风险点"], BW, BHH, CYAN, False, None, 42, 26), 118, 634, B * 3, 0.36, 34, "lt"),
    El(chip("跨校复配", ["A 校已配事项 + B 校差异 → 配置方案"], BW, BHH, ORG_L, True, None, 42, 26), 972, 634, B * 4, 0.36, 34, "lt"),
    El(txt("共同点：做错了不出门，重跑一次就行", 32, DIM2, False, shadow=False), CX, 856, B * 6, 0.4, 18),
])
# ---- 7 | 32-40s 核心章节
S(32.0, 40.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(txt("本场唯一无法自学的部分", 34, CYAN, False, shadow=False), CX, 236, 0.0, 0.34, 18),
    El(txt("给它做入职培训", 128, WHITE, glow=(0, 90, 200)), CX, 312, B * 0.5, 0.5, 42, kind="pop"),
    El(rich([("你不是在写配置，你在", DIM), ("带新人", ORG_L)], 56), CX, 526, B * 2, 0.44, 26),
    El(chip("AGENTS.md", ["员工手册 · 规范与红线"], 560, 132, CYAN, False, None, 38, 25), 128, 640, B * 3, 0.34, 26, "lt"),
    El(chip("USER.md", ["认识你这个人 · 偏好雷区"], 560, 132, CYAN, False, None, 38, 25), 680, 640, B * 4, 0.34, 26, "lt"),
    El(chip("MEMORY.md", ["工作日志 · 自己纠错"], 560, 132, ORG_L, True, None, 38, 25), 1232, 640, B * 5, 0.34, 26, "lt"),
    El(txt("写一次，以后每次都生效", 32, DIM2, False, shadow=False), CX, 838, B * 7, 0.4, 18),
])
# ---- 8 | 40-46s BREAKDOWN 数据红线
S(40.0, 46.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(pill("第 2 章 · 先立规矩", 30, ORG_L), CX, 250, 0.0, 0.4, 20),
    El(txt("数据红线", 126, WHITE, glow=(200, 90, 10)), CX, 336, B * 1, 0.6, 30),
    El(txt("客户数据 · 源码 · 投标文件", 62, ORG_L, True), CX, 556, B * 3, 0.5, 26),
    El(txt("不 进 外 部 工 具", 62, WHITE, True), CX, 664, B * 5, 0.5, 26),
    El(txt("L0 公开　·　L1 内部　·　L2 客户相关　·　L3 敏感", 32, DIM2, False, shadow=False), CX, 800, B * 7, 0.45, 18),
])
# ---- 9 | 46-52s 带走什么（riser 48-52）
S(46.0, 52.0, [
    El(LG_SM, 128, 88, 0.0, 0.3, 12, "lt"),
    El(txt("散会时你手上会有", 36, CYAN, False, shadow=False), CX, 288, 0.0, 0.34, 18),
    El(txt(["一份写着你自己名字、", "你自己规矩的配置文件"], 88, WHITE, glow=(0, 80, 190), lh=1.34), CX, 366, B * 0.5, 0.5, 34),
    El(txt("这就是你的数字员工的第一天", 42, ORG_L, True), CX, 730, B * 3, 0.44, 22),
])
# ---- 10 | 52-56s DROP 收尾金句
S(52.0, 56.0, [
    El(txt("同样的活", 120, DIM, glow=(0, 60, 150)), CX, 276, 0.02, 0.34, 30, kind="pop"),
    El(txt("不干第二遍", 176, WHITE, glow=(0, 100, 220)), CX, 420, B * 1.0, 0.5, 46, kind="pop"),
    El(txt("这才是效率的来源", 40, ORG_L, True), CX, 726, B * 3.0, 0.42, 22),
])
# ---- 11 | 56-60s 号召
S(56.0, 60.0, [
    El(txt("今天下午 · 全员参加", 66, WHITE, glow=(0, 90, 200)), CX, 306, 0.05, 0.42, 30, kind="pop"),
    El(txt("接下来是实际操作环节，别眨眼……", 40, ORG_L, True), CX, 452, B * 1.5, 0.44, 22),
    El(txt("无需提前准备　|　带上你手上正在做的活", 32, DIM2, False, shadow=False), CX, 548, B * 2.5, 0.4, 18),
    El(LG_MD, CX, 690, B * 3.5, 0.5, 24),
])

# ---------------------------------------------------------------- 背景
yy, xx = np.mgrid[0:SH, 0:SW].astype(np.float32)
ty = yy / (SH - 1)
base = np.zeros((SH, SW, 3), np.float32)
for i, (a, b) in enumerate(zip((0, 16, 34), (2, 40, 74))):
    base[:, :, i] = a + (b - a) * (ty * ty * (3 - 2 * ty))
# 细网格
gx = ((xx % 30) < 1.0).astype(np.float32)
gy = ((yy % 30) < 1.0).astype(np.float32)
base += np.clip(gx + gy, 0, 1)[:, :, None] * np.array([5, 8, 12], np.float32)
# 暗角
vg = 1.0 - 0.55 * (((xx / SW - .5) ** 2 + (yy / SH - .5) ** 2) ** 0.72) * 1.8
base *= np.clip(vg, 0.25, 1.0)[:, :, None]

def rad(cx, cy, r):
    d = np.sqrt((xx - cx) ** 2 + (yy - cy) ** 2) / r
    return np.clip(1 - d, 0, 1) ** 2.0

# 粒子
NP = 110
rng = np.random.default_rng(11)
p_x = rng.uniform(0, W, NP)
p_y = rng.uniform(0, H, NP)
p_s = rng.uniform(14, 62, NP)
p_r = rng.uniform(1.4, 3.6, NP)
p_b = rng.uniform(0.30, 1.0, NP)

FLASH = {18.0: (1.0, WHITE), 32.0: (0.55, CYAN), 40.0: (0.75, ORG_L),
         52.0: (1.0, WHITE), 56.0: (0.6, CYAN), 4.0: (0.45, CYAN), 24.0: (0.4, CYAN)}
RISER = [(14.0, 18.0), (48.0, 52.0), (30.5, 32.0)]

def ease_out(p):
    return 1 - (1 - p) ** 3

def back_out(p):
    c = 1.9
    q = p - 1
    return 1 + (c + 1) * q ** 3 + c * q ** 2

def main():
    cmd = ["ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
           "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", "%dx%d" % (W, H),
           "-r", str(FPS), "-i", "pipe:0",
           "-i", os.path.join(HERE, "bgm.wav"),
           "-t", "%.3f" % DUR,
           "-af", "afade=t=out:st=58.4:d=1.6",
           "-c:v", "libx264", "-preset", "medium", "-crf", "20",
           "-pix_fmt", "yuv420p", "-movflags", "+faststart",
           "-c:a", "aac", "-b:a", "192k",
           os.path.join(HERE, "华腾养虾记一-热场-60s.mp4")]
    pr = subprocess.Popen(cmd, stdin=subprocess.PIPE)

    si = 0
    for n in range(NF):
        t = n / FPS
        while si + 1 < len(SC) and t >= SC[si][1]:
            si += 1
        t0, t1, els = SC[si]
        lt = t - t0
        ph = t / DUR
        # 节拍相位
        bp = (t / BEAT) % 1.0
        pulse = math.exp(-bp * 5.2)

        # ---- 背景 ----
        f = base.copy()
        b1 = rad(SW * .5 + 150 * math.cos(t * .42), 70 + 40 * math.sin(t * .33), 250)
        f += b1[:, :, None] * np.array([6, 46, 116], np.float32) * (0.85 + 0.30 * pulse)
        b2 = rad(SW * .5 + 180 * math.sin(t * .29 + 2.0), SH * .92 + 30 * math.cos(t * .24), 190)
        f += b2[:, :, None] * np.array([80, 34, 6], np.float32) * (0.75 + 0.25 * pulse)
        b3 = rad(60 + 90 * math.sin(t * .21 + 4.0), SH + 20, 200)
        f += b3[:, :, None] * np.array([0, 44, 66], np.float32)

        img = Image.fromarray(np.clip(f, 0, 255).astype(np.uint8), "RGB") \
                   .resize((W, H), Image.BILINEAR)
        d = ImageDraw.Draw(img, "RGBA")

        # ---- 粒子 ----
        py = (p_y - (t * p_s)) % (H + 60) - 30
        for i in range(NP):
            r = p_r[i] * (1 + 0.5 * pulse)
            a = int(30 + 120 * p_b[i] * (0.6 + 0.4 * pulse))
            x0, y0 = p_x[i], py[i]
            d.ellipse([x0 - r, y0 - r, x0 + r, y0 + r], fill=(150, 210, 255, a))

        # ---- riser 速度线 ----
        for (rs, re) in RISER:
            if rs <= t < re:
                k = (t - re + (re - rs)) / (re - rs)
                cnt = int(10 + 46 * k)
                rr = np.random.default_rng(int(t * 60))
                for _ in range(cnt):
                    ly = rr.integers(0, H)
                    lw = rr.integers(120, 700)
                    lx = rr.integers(-200, W)
                    a = int(28 + 90 * k)
                    d.line([lx, ly, lx + lw, ly], fill=(180, 226, 255, a), width=1)
                break

        # ---- 侧边霓虹条 ----
        gl = int(70 + 130 * pulse)
        d.rectangle([0, 0, 5, H], fill=CYAN + (gl,))
        d.rectangle([W - 6, 0, W, H], fill=ORANGE + (gl,))

        # ---- 元素 ----
        for el in els:
            e_t = lt - el.d
            if e_t <= 0:
                continue
            p = min(1.0, e_t / el.dur)
            if p >= 1.0:
                img.paste(el.img, (el.x, el.y), el.img)
                continue
            av = min(1.0, p * 1.5)
            if el.kind == "pop":
                e = back_out(p)
            else:
                e = ease_out(p)
            dy = int(el.rise * (1 - e))
            lay = el.img.copy()
            lay.putalpha(el.img.getchannel("A").point(lambda v, k=av: int(v * k)))
            img.paste(lay, (el.x, el.y + dy), lay)

        # ---- 闪光 ----
        for ft, (fg, fc) in FLASH.items():
            dt = t - ft
            if 0 <= dt < 0.34:
                k = fg * (1 - dt / 0.34) ** 2.4 * 0.62
                if k > 0.004:
                    img = Image.blend(img, Image.new("RGB", (W, H), fc), k)
                    d = ImageDraw.Draw(img, "RGBA")
                break

        # ---- 进度条 + 小节刻度 ----
        d.rectangle([0, H - 6, W, H], fill=(255, 255, 255, 30))
        d.rectangle([0, H - 6, int(W * ph), H], fill=ORANGE + (240,))
        for k in range(1, 11):
            x = int(W * k / 11)
            d.rectangle([x, H - 6, x + 2, H], fill=(255, 255, 255, 70))
        d.rectangle([int(W * ph) - 3, H - 8, int(W * ph) + 3, H],
                    fill=(255, 255, 255, int(140 + 110 * pulse)))

        pr.stdin.write(img.tobytes())
        if n % 150 == 0:
            print("  %4d/%d  t=%.1fs" % (n, NF, t), flush=True)

    pr.stdin.close()
    rc = pr.wait()
    print("ffmpeg rc =", rc)

if __name__ == "__main__":
    main()
