// Adapter: 把后端 Pydantic schemas_v4 的字段转换成设计稿 JSX 期待的 mock 结构 // 保持设计稿画板组件 props 不变,转换全部发生在这一层 // M1 阶段不消费(画板还用自带 mock),M2 逐画板切换时用 const TONE_LABEL = { hard_ad: "硬广", grass: "种草", dry_goods: "干货", entertainment_hook: "娱乐" }; const RELEVANCE_LABEL = { direct: "直接", indirect: "间接", cross_industry: "可迁移", no_value: "无价值" }; const PLATFORM_HINTS = { "douyin.com": "douyin", "iesdouyin.com": "douyin", "v.douyin.com": "douyin", "xiaohongshu.com": "xhs", "xhslink.com": "xhs", "kuaishou.com": "ks", "v.kuaishou.com": "ks", "chenzhongtech.com": "ks", }; function inferPlatform(jobOrMeta) { const src = jobOrMeta?.video_filename || jobOrMeta?.link_url || ""; try { const host = new URL(src).hostname.replace(/^www\./, ""); for (const [k, v] of Object.entries(PLATFORM_HINTS)) { if (host.includes(k)) return v; } } catch (_) { /* ignore */ } if (jobOrMeta?.source === "upload") return "sph"; return "douyin"; } // jobs list -> TEARDOWN_ITEMS(拆解页左列爆款选择) function jobsToTeardownItems(jobs) { return (jobs || []).filter(j => j.has_result).map((j, i) => { const tone = j.progress?.tone_type || ""; const relev = j.progress?.relevance_type || ""; return { id: `V${String(i + 1).padStart(3, "0")}`, plat: inferPlatform(j), title: j.title || j.job_id, author: j.industry || "未分类", d1: TONE_LABEL[tone] || "", d2: RELEVANCE_LABEL[relev] || "", tag: "视频", stats: { play: "-", like: "-", cmt: "-" }, jobId: j.job_id, createdAt: j.created_at, }; }); } // AnalysisResultV4 -> 拆解详情 timeline 数据 function resultToTimeline(result) { const shots = result?.rewrite?.storyboard?.shots || []; const frames = shots.map((s, i) => ({ k: `F${i + 1}`, l: s.narration?.slice(0, 8) || `分镜${i + 1}`, time: (s.duration_sec || 0).toFixed(1) + "s", desc: s.visual_prompt || "", col: i < Math.ceil(shots.length * 0.3) ? "hook" : i > shots.length * 0.7 ? "cta" : "build", })); const points = result?.five_dimensions?.emotion_curve?.emotion_points || []; const retention = points.length >= 2 ? points.map(p => Math.round((p.intensity || 0) * 100)) : Array.from({ length: 20 }, (_, i) => 60 + Math.round(Math.sin(i / 3) * 15)); const transcript = (result?.raw_script_timed || []).map(seg => ({ t: (seg.start_sec || 0).toFixed(1) + "s", text: seg.text || "", })); return { frames, retention, transcript }; } // api_credentials + providers -> 模型管理页 img[] / vid[] 卡片 function credsToModelCards(credentials, providers) { const specByKey = Object.fromEntries((providers?.items || []).map(p => [p.key, p])); const mapKind = (provKey) => { const spec = specByKey[provKey] || {}; if (spec.kind === "multimodal_llm") return "llm"; if (spec.kind === "video_api") { if (["seedream3", "nano_banana"].includes(provKey)) return "img"; return "vid"; } return "other"; }; const toCard = (c) => { const spec = specByKey[c.provider] || {}; const modelName = c.extra?.model_name || ""; return { id: c.id, n: c.name || spec.label || c.provider, v: modelName || "-", p: spec.label?.split("(")[0] || c.provider, st: c.is_active ? "active" : (c.last_test_status === "ok" ? "standby" : "error"), use: spec.kind === "multimodal_llm" ? "拆解·多模态" : (mapKind(c.provider) === "img" ? "图文·生成" : "视频·生成"), lat: c.last_test_latency_ms ? `${c.last_test_latency_ms}ms` : "-", cost: "-", calls: "-", ok: c.last_test_status === "ok" ? 100 : (c.last_test_status ? 0 : "-"), key: c.api_key || "****", provider: c.provider, kind: mapKind(c.provider), }; }; const cards = (credentials?.items || []).map(toCard); return { img: cards.filter(c => c.kind === "img"), vid: cards.filter(c => c.kind === "vid"), llm: cards.filter(c => c.kind === "llm"), other: cards.filter(c => c.kind === "other"), all: cards, }; } // 4×4 矩阵聚合:按 tone × relevance 统计任务数量 function jobsToMatrix(jobs) { const tones = ["hard_ad", "grass", "dry_goods", "entertainment_hook"]; const relevs = ["direct", "indirect", "cross_industry", "no_value"]; const matrix = {}; tones.forEach(t => { matrix[t] = {}; relevs.forEach(r => { matrix[t][r] = { count: 0, items: [] }; }); }); (jobs || []).forEach(j => { const t = j.progress?.tone_type; const r = j.progress?.relevance_type; if (t && r && matrix[t] && matrix[t][r]) { matrix[t][r].count += 1; matrix[t][r].items.push(j); } }); return { tones, relevs, matrix, toneLabel: TONE_LABEL, relevLabel: RELEVANCE_LABEL }; } Object.assign(window, { Adapter: { inferPlatform, jobsToTeardownItems, resultToTimeline, credsToModelCards, jobsToMatrix, TONE_LABEL, RELEVANCE_LABEL, }, });