diff --git a/03_卡木(木)/木叶_视频内容/视频切片/脚本/visual_enhance.py b/03_卡木(木)/木叶_视频内容/视频切片/脚本/visual_enhance.py index 5fcc41ed..5dd264df 100644 --- a/03_卡木(木)/木叶_视频内容/视频切片/脚本/visual_enhance.py +++ b/03_卡木(木)/木叶_视频内容/视频切片/脚本/visual_enhance.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -视觉增强 v5:立体双层苹果毛玻璃舞台 +视觉增强 v6:非固定结构的立体苹果毛玻璃舞台 -特性: -1. 原视频完全不变,仅在底部叠加一块立体舞台。 -2. 舞台包含:后层底座、中层玻璃板、前层信息芯片、右上动态小视频窗。 -3. 参考卡若AI 前端的蓝/紫/青/金配色与苹果毛玻璃层级感。 -4. 全动态:缩放、漂浮、光扫、数字增长、节点扩散、图表生长、小视频窗持续动态。 +设计原则: +1. 原视频完全不变,仅底部叠加动态舞台。 +2. 每条视频自动派生独立风格:配色、窗口位置、芯片布局、信息结构都不同。 +3. 强化黑色高级感、阴影、悬浮、玻璃高光与动态小视频窗。 +4. 所有信息模块与文字均为动态生成,不使用固定平面模板。 """ import argparse +import hashlib import json import math import os @@ -29,21 +30,12 @@ if not FONTS_DIR.exists(): FONTS_DIR = Path('/Users/karuo/Documents/个人/卡若AI/03_卡木(木)/木叶_视频内容/视频切片/fonts') VW, VH = 498, 1080 -PANEL_W, PANEL_H = 418, 336 +PANEL_W, PANEL_H = 422, 340 PANEL_X = (VW - PANEL_W) // 2 -PANEL_Y = VH - PANEL_H - 28 +PANEL_Y = VH - PANEL_H - 26 FPS = 8 - -GLASS_TOP = (26, 33, 52, 214) -GLASS_BOTTOM = (18, 24, 40, 224) -GLASS_BORDER = (255, 255, 255, 70) -GLASS_EDGE = (255, 255, 255, 28) -BASE_BACK = (44, 63, 113, 90) -BASE_BACK_2 = (94, 54, 146, 80) +CURRENT_VIDEO_SEED = 'default' WHITE = (248, 250, 255, 255) -TEXT = (225, 232, 245, 255) -TEXT_SUB = (163, 176, 198, 255) -TEXT_MUTED = (104, 118, 144, 255) BLUE = (96, 165, 250, 255) PURPLE = (167, 139, 250, 255) GREEN = (52, 211, 153, 255) @@ -51,13 +43,63 @@ GOLD = (251, 191, 36, 255) ORANGE = (251, 146, 60, 255) RED = (248, 113, 113, 255) CYAN = (34, 211, 238, 255) -ACCENTS = [BLUE, PURPLE, GREEN, GOLD, ORANGE, RED, CYAN] + +PALETTES = [ + { + 'name': 'obsidian-cyan', + 'glass_top': (18, 25, 40, 212), + 'glass_bottom': (10, 14, 28, 226), + 'back_a': (35, 58, 120, 92), + 'back_b': (24, 112, 168, 70), + 'title': (246, 249, 255, 255), + 'text': (223, 231, 244, 255), + 'sub': (160, 178, 205, 255), + 'muted': (103, 119, 148, 255), + 'accents': [(98, 182, 255, 255), (46, 211, 238, 255), (139, 92, 246, 255), (250, 204, 21, 255)] + }, + { + 'name': 'midnight-violet', + 'glass_top': (28, 22, 46, 214), + 'glass_bottom': (14, 10, 26, 228), + 'back_a': (88, 54, 156, 90), + 'back_b': (44, 63, 113, 76), + 'title': (248, 247, 255, 255), + 'text': (233, 229, 248, 255), + 'sub': (184, 169, 214, 255), + 'muted': (118, 107, 144, 255), + 'accents': [(167, 139, 250, 255), (96, 165, 250, 255), (244, 114, 182, 255), (251, 191, 36, 255)] + }, + { + 'name': 'graphite-gold', + 'glass_top': (24, 24, 30, 212), + 'glass_bottom': (10, 11, 16, 228), + 'back_a': (92, 71, 29, 86), + 'back_b': (46, 70, 115, 70), + 'title': (250, 249, 245, 255), + 'text': (235, 233, 225, 255), + 'sub': (187, 180, 160, 255), + 'muted': (123, 118, 102, 255), + 'accents': [(251, 191, 36, 255), (245, 158, 11, 255), (96, 165, 250, 255), (52, 211, 153, 255)] + }, + { + 'name': 'ink-emerald', + 'glass_top': (16, 30, 28, 212), + 'glass_bottom': (9, 16, 18, 226), + 'back_a': (26, 95, 78, 90), + 'back_b': (34, 84, 131, 72), + 'title': (244, 252, 249, 255), + 'text': (218, 243, 236, 255), + 'sub': (151, 194, 183, 255), + 'muted': (103, 144, 136, 255), + 'accents': [(52, 211, 153, 255), (45, 212, 191, 255), (96, 165, 250, 255), (251, 191, 36, 255)] + }, +] def font(size: int, weight='medium'): if isinstance(weight, bool): weight = 'bold' if weight else 'medium' - font_map = { + mapping = { 'regular': [ FONTS_DIR / 'NotoSansCJK-Regular.ttc', FONTS_DIR / 'SourceHanSansSC-Medium.otf', @@ -80,8 +122,7 @@ def font(size: int, weight='medium'): Path('/System/Library/Fonts/PingFang.ttc'), ], } - candidates = font_map.get(weight, font_map['medium']) - for path in candidates: + for path in mapping.get(weight, mapping['medium']): if path.exists(): try: return ImageFont.truetype(str(path), size) @@ -90,15 +131,33 @@ def font(size: int, weight='medium'): return ImageFont.load_default() -def ease_out_cubic(t: float) -> float: +def ease_out(t: float) -> float: t = max(0.0, min(1.0, t)) - return 1 - pow(1 - t, 3) + return 1 - (1 - t) ** 3 def blend(c1, c2, t): return tuple(int(a + (b - a) * t) for a, b in zip(c1, c2)) +def hash_int(text: str) -> int: + return int(hashlib.md5(text.encode('utf-8')).hexdigest()[:8], 16) + + +def build_profile(scene, idx: int): + key = CURRENT_VIDEO_SEED + '|' + scene.get('type', '') + '|' + str(idx) + seed = hash_int(key) + palette = PALETTES[seed % len(PALETTES)] + return { + 'seed': seed, + 'palette': palette, + 'window_variant': seed % 4, + 'chip_variant': (seed // 7) % 4, + 'content_variant': (seed // 13) % 3, + 'tag_variant': (seed // 17) % 3, + } + + def rounded(draw, xy, radius, **kwargs): draw.rounded_rectangle(xy, radius=radius, **kwargs) @@ -148,152 +207,133 @@ def draw_wrap_center(draw, text, fnt, max_w, y, fill, width, line_gap=6): return y -def create_shadow(size, blur_radius=18, alpha=125): +def create_shadow(size, blur_radius=24, alpha=138): w, h = size img = Image.new('RGBA', (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) - d.rounded_rectangle((12, 18, w - 18, h - 4), radius=28, fill=(0, 0, 0, alpha)) + d.rounded_rectangle((14, 20, w - 18, h - 4), radius=30, fill=(0, 0, 0, alpha)) + d.rounded_rectangle((30, 36, w - 34, h - 16), radius=26, fill=(10, 20, 40, int(alpha * 0.25))) return img.filter(ImageFilter.GaussianBlur(blur_radius)) -def create_back_plate(progress: float): +def create_back_plate(profile, progress: float): + pal = profile['palette'] img = Image.new('RGBA', (PANEL_W, PANEL_H), (0, 0, 0, 0)) d = ImageDraw.Draw(img) pulse = 0.78 + 0.22 * math.sin(progress * math.pi * 2.0) - c1 = tuple(int(v * pulse) for v in BASE_BACK[:3]) + (BASE_BACK[3],) - c2 = tuple(int(v * pulse) for v in BASE_BACK_2[:3]) + (BASE_BACK_2[3],) - rounded(d, (18, 14, PANEL_W - 10, PANEL_H - 8), 28, fill=c1) - rounded(d, (8, 26, PANEL_W - 24, PANEL_H - 22), 24, fill=c2) - return img.filter(ImageFilter.GaussianBlur(1.2)) + c1 = tuple(int(v * pulse) for v in pal['back_a'][:3]) + (pal['back_a'][3],) + c2 = tuple(int(v * pulse) for v in pal['back_b'][:3]) + (pal['back_b'][3],) + rounded(d, (18, 12, PANEL_W - 8, PANEL_H - 8), 30, fill=c1) + rounded(d, (10, 26, PANEL_W - 24, PANEL_H - 20), 26, fill=c2) + return img.filter(ImageFilter.GaussianBlur(1.5)) -def create_glass_panel(): +def create_glass_panel(profile): + pal = profile['palette'] img = Image.new('RGBA', (PANEL_W, PANEL_H), (0, 0, 0, 0)) grad = Image.new('RGBA', (PANEL_W, PANEL_H), (0, 0, 0, 0)) gd = ImageDraw.Draw(grad) for y in range(PANEL_H): t = y / max(PANEL_H - 1, 1) - col = blend(GLASS_TOP, GLASS_BOTTOM, t) - gd.line([(0, y), (PANEL_W, y)], fill=col) + gd.line([(0, y), (PANEL_W, y)], fill=blend(pal['glass_top'], pal['glass_bottom'], t)) mask = Image.new('L', (PANEL_W, PANEL_H), 0) ImageDraw.Draw(mask).rounded_rectangle((0, 0, PANEL_W - 1, PANEL_H - 1), radius=30, fill=255) img = Image.composite(grad, img, mask) d = ImageDraw.Draw(img) - rounded(d, (0, 0, PANEL_W - 1, PANEL_H - 1), 30, outline=GLASS_BORDER, width=1) - for y in range(24): - alpha = int(34 * (1 - y / 24)) - d.line([(28, y + 2), (PANEL_W - 28, y + 2)], fill=(255, 255, 255, alpha)) - rounded(d, (10, 12, PANEL_W - 10, PANEL_H - 10), 24, outline=GLASS_EDGE, width=1) + rounded(d, (0, 0, PANEL_W - 1, PANEL_H - 1), 30, outline=(255, 255, 255, 72), width=1) + rounded(d, (10, 12, PANEL_W - 10, PANEL_H - 10), 24, outline=(255, 255, 255, 24), width=1) + for y in range(26): + alpha = int(32 * (1 - y / 26)) + d.line([(24, y + 4), (PANEL_W - 24, y + 4)], fill=(255, 255, 255, alpha)) return img -def create_chip(text, accent, active=1.0): - w = max(66, 20 + len(text) * 14) +def create_chip(text, accent, fg, active=1.0): + w = max(68, 22 + len(text) * 14) h = 34 img = Image.new('RGBA', (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) - fill = (22, 28, 45, int(185 * active)) - border = accent[:3] + (int(155 * active),) - rounded(d, (0, 0, w - 1, h - 1), 17, fill=fill, outline=border, width=1) - ff = font(14, 'medium') + rounded(d, (0, 0, w - 1, h - 1), 17, fill=(20, 24, 38, int(192 * active)), outline=accent[:3] + (int(150 * active),), width=1) + ff = font(13, 'medium') bbox = ff.getbbox(text) tw = bbox[2] - bbox[0] th = bbox[3] - bbox[1] - d.text(((w - tw) // 2, (h - th) // 2 - 1), text, font=ff, fill=(245, 248, 255, int(255 * active))) + d.text(((w - tw) // 2, (h - th) // 2 - 1), text, font=ff, fill=fg[:3] + (int(255 * active),)) return img -def create_metric_card(title, value, subtitle, accent, progress): +def create_metric_card(title, value, subtitle, accent, profile, progress): + pal = profile['palette'] w, h = 118, 84 img = Image.new('RGBA', (w, h), (0, 0, 0, 0)) d = ImageDraw.Draw(img) - fill = (18, 24, 38, 215) - rounded(d, (0, 0, w - 1, h - 1), 18, fill=fill, outline=accent[:3] + (120,), width=1) - d.rounded_rectangle((12, 14, 16, h - 14), radius=2, fill=accent[:3] + (220,)) - d.text((24, 12), title, font=font(11, 'medium'), fill=TEXT_SUB) + rounded(d, (0, 0, w - 1, h - 1), 18, fill=(18, 24, 38, 220), outline=accent[:3] + (125,), width=1) + d.rounded_rectangle((12, 14, 16, h - 14), radius=2, fill=accent[:3] + (230,)) + d.text((24, 12), title, font=font(11, 'medium'), fill=pal['sub']) d.text((24, 34), value, font=font(21, 'medium'), fill=accent) - d.text((24, 60), subtitle, font=font(10, 'regular'), fill=TEXT_MUTED) + d.text((24, 60), subtitle, font=font(10, 'regular'), fill=pal['muted']) dot_r = 5 + int(2 * math.sin(progress * math.pi * 2)) d.ellipse((w - 20 - dot_r, 12 - dot_r // 2, w - 20 + dot_r, 12 + dot_r), fill=accent[:3] + (200,)) return img -def create_video_window(scene_type: str, t: float): +def create_video_window(scene_type: str, t: float, profile): w, h = 142, 100 + pal = profile['palette'] + accents = pal['accents'] img = Image.new('RGBA', (w, h), (0, 0, 0, 0)) base = Image.new('RGBA', (w, h), (0, 0, 0, 0)) bd = ImageDraw.Draw(base) + top_bg = tuple(max(0, c - 12) for c in pal['glass_top'][:3]) + (255,) + bottom_bg = tuple(max(0, c - 8) for c in pal['glass_bottom'][:3]) + (255,) for y in range(h): ratio = y / max(h - 1, 1) - color = blend((7, 16, 32, 255), (15, 27, 55, 255), ratio) - bd.line([(0, y), (w, y)], fill=color) + bd.line([(0, y), (w, y)], fill=blend(top_bg, bottom_bg, ratio)) mask = Image.new('L', (w, h), 0) ImageDraw.Draw(mask).rounded_rectangle((0, 0, w - 1, h - 1), radius=18, fill=255) img = Image.composite(base, img, mask) d = ImageDraw.Draw(img) rounded(d, (0, 0, w - 1, h - 1), 18, outline=(255, 255, 255, 64), width=1) - scan_x = int((math.sin(t * 1.8) * 0.5 + 0.5) * (w + 40)) - 20 + scan_x = int((math.sin(t * 1.7) * 0.5 + 0.5) * (w + 44)) - 22 for i in range(-6, 7): - alpha = max(0, 35 - abs(i) * 5) + alpha = max(0, 34 - abs(i) * 5) d.line([(scan_x + i, 6), (scan_x + i - 18, h - 6)], fill=(255, 255, 255, alpha)) - for idx in range(14): - phase = t * 1.4 + idx * 0.6 - px = int((math.sin(phase * 1.2) * 0.45 + 0.5) * (w - 20)) + 10 - py = int((math.cos(phase * 0.9 + idx) * 0.35 + 0.5) * (h - 24)) + 12 - r = 2 + (idx % 2) - color = ACCENTS[idx % len(ACCENTS)][:3] + (160,) - d.ellipse((px - r, py - r, px + r, py + r), fill=color) - - if scene_type == 'title_card': - cx, cy = 48, 52 - for ring in [18, 30, 42]: - rr = ring + math.sin(t * 2.5 + ring) * 2.5 - d.ellipse((cx - rr, cy - rr, cx + rr, cy + rr), outline=(96, 165, 250, 110), width=1) - d.line((86, 26, 128, 44), fill=(167, 139, 250, 160), width=2) - d.line((86, 46, 126, 58), fill=(52, 211, 153, 160), width=2) - d.line((86, 66, 118, 80), fill=(251, 191, 36, 160), width=2) - elif scene_type == 'comparison_card': - for i in range(5): - bh = 12 + int((math.sin(t * 2 + i) * 0.5 + 0.5) * 28) - x = 16 + i * 22 - d.rounded_rectangle((x, h - 16 - bh, x + 12, h - 16), radius=4, fill=(248, 113, 113, 180)) - for i in range(5): - bh = 14 + int((math.cos(t * 2.1 + i) * 0.5 + 0.5) * 30) - x = 88 + i * 10 - d.rounded_rectangle((x, h - 18 - bh, x + 8, h - 18), radius=4, fill=(52, 211, 153, 180)) - elif scene_type == 'data_card': - for i in range(24): - x = 8 + i * 6 - base_y = 58 + math.sin(t * 2.2 + i * 0.4) * 8 - d.line((x, base_y, x, h - 10), fill=(96, 165, 250, 120), width=1) - d.line((10, 70, 40, 58, 70, 62, 102, 36, 130, 28), fill=(52, 211, 153, 210), width=3) - elif scene_type == 'flow_chart': - pts = [(20, 22), (42, 40), (68, 32), (92, 58), (116, 46)] - visible = 2 + int((math.sin(t * 1.2) * 0.5 + 0.5) * 3) - for i in range(len(pts) - 1): - color = (96, 165, 250, 180 if i < visible else 60) - d.line((pts[i], pts[i + 1]), fill=color, width=3) - for i, (px, py) in enumerate(pts): - r = 5 + int(1.5 * math.sin(t * 2 + i)) - color = (52, 211, 153, 220 if i < visible + 1 else 90) + variant = profile['window_variant'] + if variant == 0: + for idx in range(14): + phase = t * 1.4 + idx * 0.6 + px = int((math.sin(phase * 1.2) * 0.45 + 0.5) * (w - 20)) + 10 + py = int((math.cos(phase * 0.9 + idx) * 0.35 + 0.5) * (h - 24)) + 12 + r = 2 + (idx % 2) + color = accents[idx % len(accents)][:3] + (160,) d.ellipse((px - r, py - r, px + r, py + r), fill=color) - elif scene_type == 'mindmap_card': + elif variant == 1: + for i in range(5): + bh = 10 + int((math.sin(t * 2 + i) * 0.5 + 0.5) * 30) + x = 16 + i * 22 + d.rounded_rectangle((x, h - 16 - bh, x + 12, h - 16), radius=4, fill=accents[i % len(accents)][:3] + (190,)) + d.line((12, 68, 42, 54, 70, 60, 100, 32, 132, 24), fill=accents[1][:3] + (220,), width=3) + elif variant == 2: + pts = [(20, 22), (42, 40), (68, 32), (92, 58), (116, 46)] + visible = 2 + int((math.sin(t * 1.1) * 0.5 + 0.5) * 3) + for i in range(len(pts) - 1): + c = accents[i % len(accents)][:3] + (180 if i < visible else 60,) + d.line((pts[i], pts[i + 1]), fill=c, width=3) + for i, (px, py) in enumerate(pts): + r = 5 + int(1.4 * math.sin(t * 2 + i)) + color = accents[(i + 1) % len(accents)][:3] + (220 if i < visible + 1 else 90,) + d.ellipse((px - r, py - r, px + r, py + r), fill=color) + else: cx, cy = 72, 52 - d.ellipse((cx - 10, cy - 10, cx + 10, cy + 10), fill=(52, 211, 153, 220)) + d.ellipse((cx - 10, cy - 10, cx + 10, cy + 10), fill=accents[0][:3] + (220,)) for i in range(6): ang = math.radians(i * 60 + t * 18) px = cx + math.cos(ang) * 32 py = cy + math.sin(ang) * 24 - d.line((cx, cy, px, py), fill=(167, 139, 250, 120), width=2) - d.ellipse((px - 5, py - 5, px + 5, py + 5), fill=ACCENTS[i][:3] + (210,)) - else: - bar_w = 16 - for i in range(6): - level = 18 + int((math.sin(t * 2.1 + i * 0.8) * 0.5 + 0.5) * 42) - x = 16 + i * 20 - d.rounded_rectangle((x, h - 14 - level, x + bar_w, h - 14), radius=6, fill=ACCENTS[i][:3] + (180,)) + d.line((cx, cy, px, py), fill=accents[(i + 1) % len(accents)][:3] + (120,), width=2) + d.ellipse((px - 5, py - 5, px + 5, py + 5), fill=accents[i % len(accents)][:3] + (210,)) d.ellipse((12, 10, 18, 16), fill=(255, 95, 86, 220)) d.ellipse((22, 10, 28, 16), fill=(255, 189, 46, 220)) @@ -301,58 +341,111 @@ def create_video_window(scene_type: str, t: float): return img -def compose_panel(scene, scene_type, local_t, scene_progress): +def place_video_window(base, window_img, profile): + positions = [ + (PANEL_W - 158, 18), + (22, 44), + (PANEL_W - 158, 124), + (140, 18), + ] + base.alpha_composite(window_img, positions[profile['window_variant'] % len(positions)]) + + +def place_chips(base, profile, chip_specs): + variant = profile['chip_variant'] + if variant == 0: + coords = [(PANEL_W - 88 - i * 92, PANEL_H - 44) for i in range(len(chip_specs))] + elif variant == 1: + coords = [(22 + i * 94, PANEL_H - 44) for i in range(len(chip_specs))] + elif variant == 2: + coords = [(PANEL_W - 110, PANEL_H - 46 - i * 40) for i in range(len(chip_specs))] + else: + coords = [(24 + i * 82, PANEL_H - 50 - (i % 2) * 12) for i in range(len(chip_specs))] + for (txt, accent, fg), (x, y) in zip(chip_specs, coords): + chip = create_chip(txt, accent, fg, 0.96) + base.alpha_composite(chip, (x, y)) + + +def compose_panel(scene, scene_type, local_t, scene_progress, profile): + pal = profile['palette'] + accents = pal['accents'] base = Image.new('RGBA', (PANEL_W, PANEL_H), (0, 0, 0, 0)) - base.alpha_composite(create_shadow((PANEL_W, PANEL_H), blur_radius=24, alpha=120), (0, 0)) - base.alpha_composite(create_back_plate(scene_progress), (0, 0)) - base.alpha_composite(create_glass_panel(), (0, 0)) + base.alpha_composite(create_shadow((PANEL_W, PANEL_H), blur_radius=26, alpha=142), (0, 0)) + base.alpha_composite(create_back_plate(profile, scene_progress), (0, 0)) + base.alpha_composite(create_glass_panel(profile), (0, 0)) d = ImageDraw.Draw(base) - rounded(d, (18, 16, 76, 40), 12, fill=(52, 211, 153, 210)) - d.text((30, 22), 'AI', font=font(12, 'medium'), fill=WHITE) - d.text((90, 22), '卡若式视频增强', font=font(12, 'medium'), fill=TEXT_SUB) - base.alpha_composite(create_video_window(scene_type, local_t), (PANEL_W - 158, 18)) + # 顶部标签结构变化 + tag_mode = profile['tag_variant'] + if tag_mode == 0: + rounded(d, (18, 16, 76, 40), 12, fill=accents[2][:3] + (210,)) + d.text((30, 22), 'AI', font=font(12, 'medium'), fill=WHITE) + d.text((90, 22), '卡若式视频增强', font=font(12, 'regular'), fill=pal['sub']) + elif tag_mode == 1: + rounded(d, (18, 16, 108, 40), 12, fill=(255, 255, 255, 20), outline=accents[0][:3] + (120,), width=1) + d.text((30, 22), 'DYNAMIC UI', font=font(11, 'medium'), fill=pal['text']) + d.text((130, 22), pal['name'], font=font(11, 'regular'), fill=pal['muted']) + else: + rounded(d, (18, 16, 84, 40), 12, fill=(20, 24, 38, 190), outline=accents[1][:3] + (120,), width=1) + d.text((28, 22), 'FLOW', font=font(12, 'medium'), fill=pal['text']) + rounded(d, (94, 16, 152, 40), 12, fill=(255, 255, 255, 15), outline=(255, 255, 255, 30), width=1) + d.text((109, 22), 'AUTO', font=font(12, 'medium'), fill=pal['sub']) + place_video_window(base, create_video_window(scene_type, local_t, profile), profile) + + variant = profile['content_variant'] if scene_type == 'title_card': question = scene['params']['question'] subtitle = scene['params'].get('subtitle', '') - typed_ratio = ease_out_cubic(min(1.0, local_t / 1.9)) + typed_ratio = ease_out(min(1.0, local_t / 1.8)) chars = max(1, int(len(question) * typed_ratio)) q_text = question[:chars] - draw_wrap(d, q_text, font(21, 'medium'), 230, 24, 74, WHITE) + qx = 24 if variant != 1 else 34 + qy = 74 if variant == 0 else 86 + draw_wrap(d, q_text, font(21, 'medium'), 238, qx, qy, pal['title']) if typed_ratio < 1: - cursor_x = 24 + min(230, int(typed_ratio * 230)) - d.text((cursor_x, 108), '▍', font=font(18, 'regular'), fill=BLUE) + cursor_x = qx + min(238, int(typed_ratio * 220)) + d.text((cursor_x, qy + 36), '▍', font=font(16, 'regular'), fill=accents[0]) if local_t > 1.0: - alpha = ease_out_cubic(min(1.0, (local_t - 1.0) / 1.0)) - d.text((24, 144), subtitle, font=font(14, 'regular'), fill=(TEXT_SUB[0], TEXT_SUB[1], TEXT_SUB[2], int(255 * alpha))) - chip_texts = ['传统行业', '远程安装', '副业服务'] - for i, txt in enumerate(chip_texts): - ct = max(0.0, min(1.0, (local_t - 1.4 - i * 0.18) / 0.5)) - if ct > 0: - chip = create_chip(txt, ACCENTS[i], ct) - base.alpha_composite(chip, (24 + i * 96, 270 - int((1 - ct) * 14))) + alpha = ease_out(min(1.0, (local_t - 1.0) / 0.9)) + d.text((qx, qy + 72), subtitle, font=font(13, 'regular'), fill=(pal['sub'][0], pal['sub'][1], pal['sub'][2], int(255 * alpha))) + chip_texts = ['全平台', '自动承接', '后端变现'] if '发视频' in subtitle or '全链路' in subtitle else ['传统行业', '远程安装', '副业服务'] + chip_specs = [(txt, accents[i % len(accents)], pal['title']) for i, txt in enumerate(chip_texts)] + place_chips(base, profile, chip_specs) elif scene_type == 'comparison_card': params = scene['params'] - d.text((22, 70), params['title'], font=font(15, 'medium'), fill=WHITE) - rounded(d, (22, 102, 193, 286), 18, fill=(39, 19, 28, 200), outline=(248, 113, 113, 120), width=1) - rounded(d, (205, 102, 396, 286), 18, fill=(18, 42, 31, 200), outline=(52, 211, 153, 120), width=1) - d.text((36, 116), params['left_title'], font=font(13, 'medium'), fill=RED) - d.text((220, 116), params['right_title'], font=font(13, 'medium'), fill=GREEN) + d.text((22, 70), params['title'], font=font(15, 'medium'), fill=pal['title']) + if variant == 0: + left_box, right_box = (22, 102, 193, 286), (205, 102, 396, 286) + elif variant == 1: + left_box, right_box = (22, 118, 188, 292), (194, 94, 396, 278) + else: + left_box, right_box = (22, 104, 210, 270), (186, 128, 396, 294) + rounded(d, left_box, 18, fill=(39, 19, 28, 200), outline=(248, 113, 113, 120), width=1) + rounded(d, right_box, 18, fill=(18, 42, 31, 200), outline=(52, 211, 153, 120), width=1) + d.text((left_box[0] + 14, left_box[1] + 14), params['left_title'], font=font(13, 'medium'), fill=RED) + d.text((right_box[0] + 14, right_box[1] + 14), params['right_title'], font=font(13, 'medium'), fill=GREEN) for i, item in enumerate(params['left_items']): - alpha = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.4 - i * 0.15) / 0.5))) + alpha = ease_out(min(1.0, max(0.0, (local_t - 0.35 - i * 0.15) / 0.5))) if alpha > 0: - d.text((34 - int((1 - alpha) * 14), 148 + i * 34), f'✕ {item}', font=font(13, 'regular'), fill=(248, 113, 113, int(220 * alpha))) + y = left_box[1] + 46 + i * 34 + d.text((left_box[0] + 14 - int((1 - alpha) * 14), y), f'✕ {item}', font=font(13, 'regular'), fill=(248, 113, 113, int(220 * alpha))) for i, item in enumerate(params['right_items']): - alpha = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.7 - i * 0.15) / 0.5))) + alpha = ease_out(min(1.0, max(0.0, (local_t - 0.65 - i * 0.15) / 0.5))) if alpha > 0: - d.text((220 + int((1 - alpha) * 14), 148 + i * 34), f'✓ {item}', font=font(13, 'regular'), fill=(52, 211, 153, int(220 * alpha))) + y = right_box[1] + 46 + i * 34 + d.text((right_box[0] + 14 + int((1 - alpha) * 14), y), f'✓ {item}', font=font(13, 'regular'), fill=(52, 211, 153, int(220 * alpha))) elif scene_type == 'data_card': params = scene['params'] - d.text((22, 70), params['title'], font=font(15, 'medium'), fill=WHITE) - positions = [(22, 102), (148, 102), (22, 196), (148, 196)] + d.text((22, 70), params['title'], font=font(15, 'medium'), fill=pal['title']) + if variant == 0: + positions = [(22, 102), (148, 102), (22, 196), (148, 196)] + elif variant == 1: + positions = [(22, 110), (148, 94), (22, 204), (148, 188)] + else: + positions = [(30, 104), (164, 112), (22, 202), (156, 194)] for i, item in enumerate(params['items']): - card_t = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.25 - i * 0.14) / 0.55))) + card_t = ease_out(min(1.0, max(0.0, (local_t - 0.22 - i * 0.12) / 0.52))) if card_t <= 0: continue raw = str(item['number']) @@ -360,8 +453,7 @@ def compose_panel(scene, scene_type, local_t, scene_progress): if '~' in raw: try: lo, hi = raw.split('~') - lo_i, hi_i = int(lo), int(hi) - value = f"{int(lo_i * card_t)}~{int(hi_i * card_t)}" + value = f"{int(int(lo) * card_t)}~{int(int(hi) * card_t)}" except Exception: pass elif raw.endswith('万+'): @@ -376,45 +468,48 @@ def compose_panel(scene, scene_type, local_t, scene_progress): value = f"{max(1, int(num * card_t))}分钟" except Exception: pass - metric = create_metric_card(item['label'], value, item['desc'], ACCENTS[i], local_t) + metric = create_metric_card(item['label'], value, item['desc'], accents[i % len(accents)], profile, local_t) mx, my = positions[i] base.alpha_composite(metric, (mx, my - int((1 - card_t) * 10))) elif scene_type == 'flow_chart': params = scene['params'] - d.text((22, 70), params['title'], font=font(15, 'medium'), fill=WHITE) - start_y = 112 + d.text((22, 70), params['title'], font=font(15, 'medium'), fill=pal['title']) + start_y = 112 if variant != 2 else 102 + step_gap = 42 if variant == 0 else 38 for i, step in enumerate(params['steps']): - st = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.18 - i * 0.18) / 0.55))) + st = ease_out(min(1.0, max(0.0, (local_t - 0.16 - i * 0.17) / 0.52))) if st <= 0: continue - y = start_y + i * 42 - accent = ACCENTS[i] - cx = 38 + y = start_y + i * step_gap + accent = accents[i % len(accents)] + cx = 38 if variant != 1 else 54 cy = y + 12 - d.ellipse((cx - 12, cy - 12, cx + 12, cy + 12), fill=(accent[0], accent[1], accent[2], 220)) + d.ellipse((cx - 12, cy - 12, cx + 12, cy + 12), fill=accent[:3] + (220,)) d.text((cx - 4, cy - 8), str(i + 1), font=font(12, 'medium'), fill=(255, 255, 255, int(255 * st))) - d.text((64 + int((1 - st) * 18), y), step, font=font(14, 'regular'), fill=(TEXT[0], TEXT[1], TEXT[2], int(255 * st))) + d.text((cx + 26 + int((1 - st) * 16), y), step, font=font(14, 'regular'), fill=(pal['text'][0], pal['text'][1], pal['text'][2], int(255 * st))) if i < len(params['steps']) - 1: - for dy in range(22, 40, 5): - d.ellipse((37, y + dy, 39, y + dy + 2), fill=(255, 255, 255, 45)) + for dy in range(22, step_gap - 2, 5): + d.ellipse((cx - 1, y + dy, cx + 1, y + dy + 2), fill=(255, 255, 255, 46)) elif scene_type == 'mindmap_card': params = scene['params'] - cx, cy = 136, 192 - center_t = ease_out_cubic(min(1.0, local_t / 0.8)) - r = 34 + int(3 * math.sin(local_t * 2)) - d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=(52, 211, 153, int(220 * center_t))) + cx, cy = (150, 188) if variant == 0 else ((124, 200) if variant == 1 else (140, 178)) + center_t = ease_out(min(1.0, local_t / 0.8)) + r = 32 + int(4 * math.sin(local_t * 2)) + d.ellipse((cx - r, cy - r, cx + r, cy + r), fill=accents[2][:3] + (int(220 * center_t),)) bb = font(14, 'medium').getbbox(params['center']) d.text((cx - (bb[2] - bb[0]) // 2, cy - 8), params['center'], font=font(14, 'medium'), fill=WHITE) branches = params['branches'] + angle_step = 42 if variant == 0 else (34 if variant == 1 else 48) + start_angle = -100 if variant != 2 else -120 for i, br in enumerate(branches): - bt = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.4 - i * 0.12) / 0.6))) + bt = ease_out(min(1.0, max(0.0, (local_t - 0.36 - i * 0.11) / 0.56))) if bt <= 0: continue - ang = math.radians(-100 + i * 36) + ang = math.radians(start_angle + i * angle_step) dist = 92 * bt bx = cx + int(math.cos(ang) * dist) by = cy + int(math.sin(ang) * dist) - accent = ACCENTS[i] + accent = accents[i % len(accents)] d.line((cx, cy, bx, by), fill=(accent[0], accent[1], accent[2], int(130 * bt)), width=2) ff = font(11, 'medium') bbox = ff.getbbox(br) @@ -425,37 +520,40 @@ def compose_panel(scene, scene_type, local_t, scene_progress): d.text((bx - tw // 2, by - th // 2), br, font=ff, fill=(accent[0], accent[1], accent[2], 255)) else: params = scene['params'] - rounded(d, (22, 60, PANEL_W - 22, 114), 22, fill=(18, 27, 44, 220), outline=(96, 165, 250, 110), width=1) - draw_wrap_center(d, params['headline'], font(21, 'medium'), PANEL_W - 80, 74, WHITE, PANEL_W) + rounded(d, (22, 60, PANEL_W - 22, 114), 22, fill=(18, 27, 44, 220), outline=accents[0][:3] + (110,), width=1) + draw_wrap_center(d, params['headline'], font(21, 'medium'), PANEL_W - 80, 74, pal['title'], PANEL_W) y = 132 for i, item in enumerate(params['points']): - alpha = ease_out_cubic(min(1.0, max(0.0, (local_t - 0.4 - i * 0.14) / 0.5))) + alpha = ease_out(min(1.0, max(0.0, (local_t - 0.35 - i * 0.13) / 0.48))) if alpha <= 0: continue - accent = ACCENTS[i] + accent = accents[i % len(accents)] d.ellipse((30, y + i * 34 + 6, 38, y + i * 34 + 14), fill=(accent[0], accent[1], accent[2], int(220 * alpha))) - d.text((48 + int((1 - alpha) * 12), y + i * 34), item, font=font(14, 'regular'), fill=(TEXT[0], TEXT[1], TEXT[2], int(255 * alpha))) - cta_t = ease_out_cubic(min(1.0, max(0.0, (local_t - 1.2) / 0.5))) + d.text((48 + int((1 - alpha) * 12), y + i * 34), item, font=font(14, 'regular'), fill=(pal['text'][0], pal['text'][1], pal['text'][2], int(255 * alpha))) + cta_t = ease_out(min(1.0, max(0.0, (local_t - 1.1) / 0.45))) if cta_t > 0: - rounded(d, (42, 286 - int((1 - cta_t) * 8), PANEL_W - 42, 316 - int((1 - cta_t) * 8)), 16, - fill=(52, 211, 153, int(220 * cta_t))) + rounded(d, (42, 286 - int((1 - cta_t) * 8), PANEL_W - 42, 316 - int((1 - cta_t) * 8)), 16, fill=accents[2][:3] + (int(220 * cta_t),)) draw_center(d, params['cta'], font(13, 'medium'), 293 - int((1 - cta_t) * 8), WHITE, PANEL_W) - chips = [('AI安装', BLUE), ('远程交付', PURPLE), ('可变现', GREEN)] - for i, (txt, accent) in enumerate(chips): - ct = create_chip(txt, accent, 0.96) - base.alpha_composite(ct, (PANEL_W - 86 - i * 94, PANEL_H - 44)) + chip_specs = [ + ('内容引擎', accents[0], pal['title']), + ('自动分发', accents[1], pal['title']), + ('后端承接', accents[2], pal['title']), + ] + if '全链路' in json.dumps(scene.get('params', {}), ensure_ascii=False): + chip_specs = [('全平台', accents[0], pal['title']), ('小程序', accents[1], pal['title']), ('变现链路', accents[2], pal['title'])] + place_chips(base, profile, chip_specs) return base -def render_overlay_frame(scene, local_t): +def render_overlay_frame(scene, local_t, profile): scene_progress = (local_t % 6.0) / 6.0 - panel = compose_panel(scene, scene['type'], local_t, scene_progress) - intro = ease_out_cubic(min(1.0, local_t / 0.7)) - breath = 1 + math.sin(local_t * 1.4) * 0.012 - scale = (0.94 + intro * 0.06) * breath - y_offset = int((1 - intro) * 20 + math.sin(local_t * 1.1) * 4) - x_offset = int(math.sin(local_t * 0.7) * 2) + panel = compose_panel(scene, scene['type'], local_t, scene_progress, profile) + intro = ease_out(min(1.0, local_t / 0.65)) + breath = 1 + math.sin(local_t * 1.35) * 0.013 + scale = (0.935 + intro * 0.07) * breath + y_offset = int((1 - intro) * 18 + math.sin(local_t * 1.05) * 5) + x_offset = int(math.sin(local_t * 0.75) * 3) panel_resized = panel.resize((int(PANEL_W * scale), int(PANEL_H * scale)), Image.LANCZOS) frame = Image.new('RGBA', (VW, VH), (0, 0, 0, 0)) px = (VW - panel_resized.width) // 2 + x_offset @@ -465,27 +563,12 @@ def render_overlay_frame(scene, local_t): DEFAULT_SCENES = [ - {'start': 0, 'end': 30, 'type': 'title_card', - 'params': {'question': '帮别人装AI,一单能挣多少钱?', 'subtitle': '传统行业也能做的AI副业'}}, - {'start': 30, 'end': 70, 'type': 'comparison_card', - 'params': {'title': '营销号 vs 真实情况', - 'left_title': '营销号', 'left_items': ['AI暴富神话', '零成本躺赚', '一夜翻身'], - 'right_title': '真实可做', 'right_items': ['帮装AI工具', '卖API接口', '远程安装服务']}}, - {'start': 70, 'end': 130, 'type': 'data_card', - 'params': {'title': '装AI服务 · 核心数据', 'items': [ - {'number': '300~1000', 'label': '元/单', 'desc': '远程安装AI工具'}, - {'number': '170万+', 'label': '淘宝月销最高', 'desc': '帮别人装AI的店铺'}, - {'number': '30分钟', 'label': '单次耗时', 'desc': '远程操作即可完成'}, - {'number': '全平台', 'label': '淘宝/闲鱼/Soul', 'desc': '多渠道接单'}]}}, - {'start': 130, 'end': 190, 'type': 'flow_chart', - 'params': {'title': '装AI赚钱 · 操作步骤', - 'steps': ['开淘宝/闲鱼店铺', '标题写清:AI安装服务', '客户下单 远程连接', '30分钟完成安装', '收款300~1000元']}}, - {'start': 190, 'end': 230, 'type': 'mindmap_card', - 'params': {'center': '装AI副业', 'branches': ['淘宝开店', '闲鱼挂单', 'Soul接客', '远程安装', 'Mac工具', '月入可观']}}, - {'start': 230, 'end': 245, 'type': 'summary_card', - 'params': {'headline': '赚钱没那么复杂', - 'points': ['帮人装AI 一单300~1000', '淘宝最高店月销170万+', '30分钟远程安装搞定', '开店+接单+装机=副业'], - 'cta': '关注了解更多AI副业'}}, + {'start': 0, 'end': 30, 'type': 'title_card', 'params': {'question': '帮别人装AI,一单能挣多少钱?', 'subtitle': '传统行业也能做的AI副业'}}, + {'start': 30, 'end': 70, 'type': 'comparison_card', 'params': {'title': '营销号 vs 真实情况', 'left_title': '营销号', 'left_items': ['AI暴富神话', '零成本躺赚', '一夜翻身'], 'right_title': '真实可做', 'right_items': ['帮装AI工具', '卖API接口', '远程安装服务']}}, + {'start': 70, 'end': 130, 'type': 'data_card', 'params': {'title': '装AI服务 · 核心数据', 'items': [{'number': '300~1000', 'label': '元/单', 'desc': '远程安装AI工具'}, {'number': '170万+', 'label': '淘宝月销最高', 'desc': '帮别人装AI的店铺'}, {'number': '30分钟', 'label': '单次耗时', 'desc': '远程操作即可完成'}, {'number': '全平台', 'label': '淘宝/闲鱼/Soul', 'desc': '多渠道接单'}]}}, + {'start': 130, 'end': 190, 'type': 'flow_chart', 'params': {'title': '装AI赚钱 · 操作步骤', 'steps': ['开淘宝/闲鱼店铺', '标题写清:AI安装服务', '客户下单 远程连接', '30分钟完成安装', '收款300~1000元']}}, + {'start': 190, 'end': 230, 'type': 'mindmap_card', 'params': {'center': '装AI副业', 'branches': ['淘宝开店', '闲鱼挂单', 'Soul接客', '远程安装', 'Mac工具', '月入可观']}}, + {'start': 230, 'end': 245, 'type': 'summary_card', 'params': {'headline': '赚钱没那么复杂', 'points': ['帮人装AI 一单300~1000', '淘宝最高店月销170万+', '30分钟远程安装搞定', '开店+接单+装机=副业'], 'cta': '关注了解更多AI副业'}}, ] @@ -495,11 +578,12 @@ def render_scene_video(scene, tmp_dir, idx): os.makedirs(scene_dir, exist_ok=True) frames = max(1, int(duration * FPS)) concat_lines = [] - print(f" [{idx+1}] {scene['type']} {scene['start']:.0f}s-{scene['end']:.0f}s ({frames} 帧)...", end='', flush=True) + profile = build_profile(scene, idx) + print(f" [{idx+1}] {scene['type']} {scene['start']:.0f}s-{scene['end']:.0f}s ({frames} 帧, {profile['palette']['name']})...", end='', flush=True) last_fp = None for i in range(frames): local_t = i / FPS - frame = render_overlay_frame(scene, local_t) + frame = render_overlay_frame(scene, local_t, profile) fp = os.path.join(scene_dir, f'f_{i:04d}.png') frame.save(fp, 'PNG') concat_lines.append(f"file '{fp}'") @@ -552,13 +636,7 @@ def build_full_overlay(scene_videos, duration, tmp_dir): def compose_final(input_video, overlay_video, output_path, duration): - cmd = [ - 'ffmpeg', '-y', '-i', input_video, '-i', overlay_video, - '-filter_complex', '[1:v]format=rgba[ov];[0:v][ov]overlay=0:0:format=auto:shortest=1[v]', - '-map', '[v]', '-map', '0:a?', - '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', - '-c:a', 'aac', '-b:a', '128k', '-t', f'{duration:.3f}', '-movflags', '+faststart', output_path, - ] + cmd = ['ffmpeg', '-y', '-i', input_video, '-i', overlay_video, '-filter_complex', '[1:v]format=rgba[ov];[0:v][ov]overlay=0:0:format=auto:shortest=1[v]', '-map', '[v]', '-map', '0:a?', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-c:a', 'aac', '-b:a', '128k', '-t', f'{duration:.3f}', '-movflags', '+faststart', output_path] print(' 合成最终视频...', end='', flush=True) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: @@ -575,11 +653,13 @@ def get_duration(video_path): def main(): - parser = argparse.ArgumentParser(description='视觉增强 v5:立体苹果毛玻璃舞台') + global CURRENT_VIDEO_SEED + parser = argparse.ArgumentParser(description='视觉增强 v6:非固定结构高级玻璃舞台') parser.add_argument('-i', '--input', required=True) parser.add_argument('-o', '--output', required=True) parser.add_argument('--scenes') args = parser.parse_args() + CURRENT_VIDEO_SEED = Path(args.input).stem scenes = DEFAULT_SCENES if args.scenes and os.path.exists(args.scenes): with open(args.scenes, 'r', encoding='utf-8') as f: @@ -589,8 +669,8 @@ def main(): scene['end'] = min(scene['end'], duration) os.makedirs(os.path.dirname(args.output) or '.', exist_ok=True) print(f"输入: {os.path.basename(args.input)} ({duration:.0f}s)") - print(f"场景: {len(scenes)} · 立体双层毛玻璃舞台 · 全动态\n") - with tempfile.TemporaryDirectory(prefix='ve5_') as tmp_dir: + print(f"场景: {len(scenes)} · 非固定结构 · 每视频独立风格\n") + with tempfile.TemporaryDirectory(prefix='ve6_') as tmp_dir: print('【1/3】生成每段动态舞台...', flush=True) scene_videos = [] for idx, scene in enumerate(scenes): diff --git a/运营中枢/工作台/gitea_push_log.md b/运营中枢/工作台/gitea_push_log.md index c60c5f87..c84f7ac0 100644 --- a/运营中枢/工作台/gitea_push_log.md +++ b/运营中枢/工作台/gitea_push_log.md @@ -286,3 +286,4 @@ | 2026-03-11 09:56:46 | 🔄 卡若AI 同步 2026-03-11 09:56 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | | 2026-03-11 13:54:29 | 🔄 卡若AI 同步 2026-03-11 13:54 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | | 2026-03-11 14:38:13 | 🔄 卡若AI 同步 2026-03-11 14:38 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | +| 2026-03-11 14:45:54 | 🔄 卡若AI 同步 2026-03-11 14:45 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | diff --git a/运营中枢/工作台/代码管理.md b/运营中枢/工作台/代码管理.md index bba99653..5efc68cf 100644 --- a/运营中枢/工作台/代码管理.md +++ b/运营中枢/工作台/代码管理.md @@ -289,3 +289,4 @@ | 2026-03-11 09:56:46 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-11 09:56 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-03-11 13:54:29 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-11 13:54 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | | 2026-03-11 14:38:13 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-11 14:38 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) | +| 2026-03-11 14:45:54 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-11 14:45 | 更新:卡木、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |