From 07fbcd489aca8c6bd1daabcc83eaeb75543cd6f2 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Fri, 4 Sep 2026 15:25:22 +0800 Subject: [PATCH] =?UTF-8?q?R59b:=20=E8=A7=86=E9=A2=91=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=8D=A1=E5=AE=9A=E7=A8=BF=E7=99=BD=E5=BA=95?= =?UTF-8?q?(=E7=94=A8=E6=88=B7=E6=94=B9=E7=A8=BF:bgGrad=20=E7=BA=AF?= =?UTF-8?q?=E7=99=BD=20+=20accent=20=E5=8F=8C=E7=B3=BB=20tint=20=E5=8D=A1,?= =?UTF-8?q?=20F6=E7=A6=81=E6=AD=A2=E8=B4=B4=E9=9D=A0=E5=B7=B2=E8=90=BD?= =?UTF-8?q?=E5=AE=9E,=20cdp-verify-r59.mjs=20=E5=9B=9E=E5=BD=92=E8=84=9A?= =?UTF-8?q?=E6=9C=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .workbuddy/memory/2026-09-04.md | 13 +++ .../V1.0/mcn-work-shop/cdp-verify-r59.mjs | 80 +++++++++++++++++++ .../V1.0/mcn-work-shop/public/data-pages.js | 21 ++--- .../V1.0/mcn-work-shop/public/index.html | 2 +- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-r59.mjs diff --git a/.workbuddy/memory/2026-09-04.md b/.workbuddy/memory/2026-09-04.md index d97c3d2..07b0728 100644 --- a/.workbuddy/memory/2026-09-04.md +++ b/.workbuddy/memory/2026-09-04.md @@ -68,3 +68,16 @@ - **实测(9223,fake 全清)**:①多版本(video 190 4 版)删第 4/4 → 弹窗重建 3/3 内容切 rw25 ✅;②单版本视频脚本 isLast → router 刷新 ✅;③自定义选题 id38(video_id 空)查看弹窗含删除按钮 → 确认删除 → 弹窗关+行消失,DB 复查 rewrite/review/storyboard 三级全 0 ✅ - **版本号**:data-pages.js bump ?v=20260904e→f(静态 js 实时读盘未重启) - **遗留**:R55-R58 改动未 commit(待 git 提交);帮助文档一行两卡 + 模拟操作指引点击无效果仍待修 + +## R59 视频关键信息卡迭代收尾(15:30,全路径实测通过) +- **用户诉求递进**(多轮反馈):①"在视频详情页拆解tab右侧加视频关键信息卡片"(已有实现)→ ②"加上可复用爆款内容" → ③"现在这个太难看了,参考账号设定卡的卡片" → ④"每一个卡片的内容都顶着卡片上边" → ⑤"内容结构板块也要加进去" → ⑥"视频解析的卡片用白色背景就行"(白底定稿,去除账号设定卡式渐变大底) +- **三处代码改动(public/data-pages.js)**: + 1. **vkCollect 内容结构板块全量化**:拆解表「内容结构」(段落板块|维度|内容)作为独立 `kind:'struct'` 整卡(板块≤7、行全保留、值折行≤3+省略号);无内容结构表时回退板块式框架表 → `kind:'blocks'` 速览;`mods` 装配顺序:内容定位→可复用爆款内容→**内容结构**→风格与传播→台词金句 + 2. **renderVideoKeySvg 布局重写(统一"文字顶游标"排版)**:`vPadLR=16, vPadT=15, vPadB=14, insetTop=size*0.9`;kv 卡 label 22 行盒/正文 17、struct 卡板块头 23(左侧 boxTag 竖条 4×14 rx2 + 文字 x+13)、blocks 卡 padT=13 padLR=12;卡内首行 gap=vPadT+insetTop≈26(之前 ≈12 贴顶)—— F6 规范"禁止贴靠"统一落实 + 3. **色板白底定稿**:`bgA=bgB=#FFFFFF`(两 stop 纯白);deep 暖米 accent(#FBF3E7 box / #C13B2F boxTag / #7B2D1B boxTitle / 渐变标题 #8C2F1E→#C05A32);light 粉白 accent(#FDF1F5 box / #D84A8A boxTag / #A13D6B boxTitle / 渐变 #B03E76→#E0579A);模块标题色 secTitle=accent 深色(白底上不再用浅色字);header 框同色 tint 形成"主卡"质感 +- **数据形态主目标确认**:13 维 MCP 拆解(赛道/账号核心定位/内容核心/可复用的爆款内容体系/…/内容结构/框架/事件/梗元素)—— v93/v190 实测标准形态;v205 类 F4 形态(无 tag 行无内容结构)不在本卡范围(keyCardHtml='' → 自动 fallback 拆解全文 md) +- **版本号**:data-pages.js bump `?v=20260904i→j`,index.html 同步 +- **回归脚本 cdp-verify-r59.mjs**(根目录):9223 真实渲染 → 审计模块齐全/卡内首行 gap≥20/不越出 viewBox/bgGrad stops #FFFFFF → v93(vbH 1719 4 模块 4 板块 5 卡 gap 26 全绿)+ v190(vbH 1766 同构)→ `=== PASS ===` +- **产物**:`tmp/r59_v93_card.svg` + `tmp/r59_v190_card.svg`(白底定稿版完整 SVG 源);旧 `tmp/v190_keycard.svg`/`v93_keycard.svg` 渐变版已清 +- **环境复用**:9223 专用 Chrome profile `C:/Users/maidou/.workbuddy/chrome-9223-profile`(已 9-02 授权过);会话间被回收时需 `run_in_background` 重启 chrome 进程(bash `&` 会随沙箱会话结束被杀) +- **R55-R58 同步清理**:cdp-verify-r59.mjs 与既有 cdp-verify-r28 等同级保留为回归;commit/push 待办 diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-r59.mjs b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-r59.mjs new file mode 100644 index 0000000..8855d7a --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-r59.mjs @@ -0,0 +1,80 @@ +// R59 关键信息卡(白底版)真实渲染验证:拆解 tab 默认卡视图 +// 断言:① 卡片 SVG 渲染且含「内容结构」整卡(板块小节齐全)② 卡内首行文字不贴顶(gap≥20) +// ③ 文本不越出 viewBox ④ 深浅两系均出卡;并截图存档 +const CDP = 9223; +const list = await fetch(`http://localhost:${CDP}/json`).then((r) => r.json()); +const page = list.find((t) => t.type === 'page' && (t.url || '').includes('8900')) || list.find((t) => t.type === 'page'); +if (!page) { console.log('ERR: no page'); process.exit(1); } +const ws = new WebSocket(page.webSocketDebuggerUrl); +let id = 0; const pending = new Map(); +const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); }); +ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } }; +await new Promise((r) => (ws.onopen = r)); +const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; }; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +await send('Page.enable'); await send('Runtime.enable'); +await send('Emulation.setDeviceMetricsOverride', { width: 860, height: 1000, deviceScaleFactor: 1, mobile: false }); +const waitFor = async (expr, ms = 15000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; }; +const fail = (m) => { console.log('FAIL@' + m); process.exit(1); }; +const fs = await import('node:fs'); + +const AUDIT = `(() => { + const svg = document.querySelector('.vk-view svg'); + if (!svg) return { err: 'no svg in .vk-view' }; + const vbH = svg.viewBox.baseVal.height; + const rects = [...svg.querySelectorAll('rect')].map((r) => ({ x: Math.round(r.x.baseVal.value * 10) / 10, y: Math.round(r.y.baseVal.value * 10) / 10, w: Math.round(r.width.baseVal.value), h: Math.round(r.height.baseVal.value), fill: r.getAttribute('fill') })); + const texts = [...svg.querySelectorAll('text')].map((t) => ({ x: +t.getAttribute('x'), y: +t.getAttribute('y'), size: +t.getAttribute('font-size'), wt: t.getAttribute('font-weight'), txt: (t.textContent || '').slice(0, 42) })); + const titles = texts.filter((t) => t.txt.startsWith('▍')).map((t) => t.txt.slice(1)); + const cards = rects.filter((r) => (r.fill || '').startsWith('#F') && r.w > 500 && r.h > 40); /* 整宽 tint 内容卡 */ + const audits = cards.map((r) => { + const inT = texts.filter((t) => t.y > r.y + 2 && t.y < r.y + r.h && t.x >= r.x - 1 && t.x <= r.x + r.w + 1).sort((a, b) => a.y - b.y); + const first = inT[0]; + return { cardTop: r.y, h: r.h, firstTxt: first ? first.txt : '', gap: first ? Math.round((first.y - r.y) * 10) / 10 : null }; + }); + const maxTextY = Math.max(...texts.map((t) => t.y)); + /* 内容结构整卡内应出现的板块名(struct 板块头 text 加粗、位于该卡内且不重复) */ + const structCard = cards[1]; /* 第二张卡(内容定位卡之后)可能为内容结构;改用标题定位 */ + const titleYs = texts.filter((t) => t.txt.startsWith('▍')).map((t) => ({ t: t.txt.slice(1), y: t.y })); + const cardByTitle = {}; + cards.forEach((r) => { const t = titleYs.find((x) => x.y < r.y && r.y - x.y < 60); if (t) cardByTitle[t.t] = r; }); + const secCard = cardByTitle['内容结构']; + const secBoard = secCard ? texts.filter((t) => t.y > secCard.y && t.y < secCard.y + secCard.h && (t.wt || '') === '700' && t.x > secCard.x && t.x < secCard.x + 120).map((t) => t.txt) : []; + const bgStops = [...svg.querySelectorAll('#bgGrad stop')].map((s) => s.getAttribute('stop-color')); + return { vbH, titles, nCards: cards.length, audits, cardByTitle: Object.keys(cardByTitle), secBoard, maxTextY, over: maxTextY > vbH - 4, bgStops }; +})()`; + +const VIDS = [93, 190]; +const out = {}; +for (const id of VIDS) { + await send('Page.navigate', { url: `http://127.0.0.1:8900/#/video/${id}` }); + /* 等路由渲染完成:hash 正确 + 视频信息卡渲染(旧 DOM 残留会误判,需等新内容) */ + if (!(await waitFor(`location.hash === '#/video/${id}' && !!document.querySelector('#view .video-grid')`, 15000))) fail(`v${id} route`); + await sleep(400); + await ev(`[...document.querySelectorAll('#view .tabs .tab')].find((t) => t.textContent.trim() === '视频拆解').click()`); + const hasCard = await waitFor(`!!document.querySelector('#view .vk-view svg')`, 12000); + if (!hasCard) { out['v' + id] = { __err: 'no keycard svg (纯 md fallback?)', hasMd: await ev(`!!document.querySelector('.vk-view .md-body, .vk-text')`) }; continue; } + await sleep(500); + const audit = await ev(AUDIT); + out['v' + id] = audit; + /* 截图:滚动 svg 到顶部后按元素区域截图 */ + await ev(`document.querySelector('.vk-view svg').scrollIntoView({ block: 'start' })`); await sleep(250); + const rect = await ev(`(() => { const r = document.querySelector('.vk-view svg').getBoundingClientRect(); return { x: r.x, y: r.y, w: r.width, h: r.height }; })()`); + if (rect && !rect.__err && rect.w > 100) { + const shot = await send('Page.captureScreenshot', { format: 'png', clip: { x: Math.max(0, rect.x - 4), y: Math.max(0, rect.y - 4), width: Math.min(rect.w + 8, 1200), height: Math.min(rect.h + 8, 4000), scale: 1 } }); + fs.writeFileSync(`tmp/r59_card_v${id}.png`, Buffer.from(shot.data, 'base64')); + out['v' + id].shot = `tmp/r59_card_v${id}.png`; + } +} + +console.log(JSON.stringify(out, null, 1)); +const v93 = out.v93, v190 = out.v190; +const ok = v93 && !v93.__err && v190 && !v190.__err + && v93.bgStops && v93.bgStops.length === 2 && v93.bgStops.every((c) => c === '#FFFFFF') + && v190.bgStops && v190.bgStops.length === 2 && v190.bgStops.every((c) => c === '#FFFFFF') + && v93.titles.includes('内容结构') && v190.titles.includes('内容结构') + && v93.secBoard && v93.secBoard.some((b) => b.includes('开场钩子')) && v93.secBoard.some((b) => b.includes('中段叙事')) + && v93.audits.every((a) => a.gap >= 20) && v190.audits.every((a) => a.gap >= 20) + && !v93.over && !v190.over; +console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ==='); +ws.close(); +process.exit(ok ? 0 : 1); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js index 43b7d5e..0e4b4c5 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -1289,23 +1289,24 @@ function vkCollect(analysis) { return { track, mods }; } /* SVG 生成器:analysis=拆解 dict,vt=视频标题(含话题标签原样,卡内自行清理),acc=账号名。 - 排版模型:全宽 680,头部标题框 520×170(同账号设定卡),其下逐模块排版——模块标题(底色上书写)→ 内容卡(圆角白/米卡)。 + 排版模型:全宽 680,白色画布(用户定稿:白底简洁版,非账号设定卡式渐变大底),头部标题框 520×170, + 其下逐模块排版——模块标题(accent 色书写)→ 浅 tint 圆角内容卡;深/浅调性只作用于 accent 与卡片 tint。 文本折行按「全角单位」计算:中文 1u≈字号宽、ASCII 0.55u;行 y 均为 text 基线,行高按字号 + 视觉间距直接给出(无字符串后处理)。 */ function renderVideoKeySvg(analysis, vt, acc) { const D = vkCollect(analysis); if (!D || !D.mods || !D.mods.length) return ''; const deep = vkPalette(analysis) === 'deep'; - /* 色板(复用账号设定卡深浅两系规范) */ + /* 色板:白底 + tint 卡;deep=暖米棕红系(深沉向)/ light=粉白玫瑰金系(明快向) */ const P = deep ? { - bgA: '#2B0F0A', bgB: '#1A0805', /* 深棕红渐变底(深沉向) */ - box: '#FAEEDA', boxBd: '#EDCBA4', boxTitle: '#7B2D1B', boxText: '#6F5B4A', boxTag: '#C13B2F', - headBox: '#FAEEDA', headBd: '#EDCBA4', headTitleA: '#8C2F1E', headTitleB: '#C05A32', headSub: '#8C2F1E', headDim: '#A88770', - secTitle: '#FAEEDA', deco: '#A88770', + bgA: '#FFFFFF', bgB: '#FFFFFF', /* 纯白底(09-04 用户定稿) */ + box: '#FBF3E7', boxBd: '#EBD9BC', boxTitle: '#7B2D1B', boxText: '#6A5644', boxTag: '#C13B2F', + headBox: '#FBF3E7', headBd: '#EBD9BC', headTitleA: '#8C2F1E', headTitleB: '#C05A32', headSub: '#C13B2F', headDim: '#A08B76', + secTitle: '#8C2F1E', deco: '#EBD9BC', } : { - bgA: '#D6A3A9', bgB: '#9A5563', /* 玫瑰金渐变底(明快向,王微斯定稿) */ - box: '#FFFFFF', boxBd: '#E8C9DC', boxTitle: '#7B2D5E', boxText: '#6F4A5E', boxTag: '#E0437F', - headBox: '#FFFFFF', headBd: '#E8C9DC', headTitleA: '#C0457F', headTitleB: '#E876A9', headSub: '#7B2D5E', headDim: '#B98AA6', - secTitle: '#FDE8EC', deco: '#EAC6CE', + bgA: '#FFFFFF', bgB: '#FFFFFF', + box: '#FDF1F5', boxBd: '#F0D3E0', boxTitle: '#A13D6B', boxText: '#6E4A5C', boxTag: '#D84A8A', + headBox: '#FDF1F5', headBd: '#F0D3E0', headTitleA: '#B03E76', headTitleB: '#E0579A', headSub: '#D84A8A', headDim: '#B193A5', + secTitle: '#A13D6B', deco: '#F0D3E0', }; const W = 680, LX = 40, CW = W - LX * 2; /* 内容区:x 40,宽 600 */ const X = (u) => Math.max(4, Math.floor(u)); /* 全角单位 → 字数 */ diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html index ca1c09f..66fefc0 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/index.html @@ -43,6 +43,6 @@ - +