完善二期清单

This commit is contained in:
Azmat@qq.com
2026-08-18 14:11:05 +08:00
parent d1ecb52125
commit dfe94a923e
55 changed files with 1754 additions and 137 deletions
+35
View File
@@ -0,0 +1,35 @@
// 端到端跑一次「合成完整视频」:提交 → 轮询进度 → 拿成片地址(验证 ffmpeg 真拼出来了)
const API = process.env.API || "http://127.0.0.1:8010";
const PROJECT = process.argv[2];
if (!PROJECT) throw new Error("用法: node _merge-e2e.mjs <projectId>");
const login = await (await fetch(`${API}/api/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "airshelf", password: "Restraint2026" })
})).json();
const auth = { Authorization: `Token ${login.token}`, "Content-Type": "application/json" };
const t0 = Date.now();
const submit = await fetch(`${API}/api/projects/${PROJECT}/submit-export/`, { method: "POST", headers: auth });
console.log("submit:", submit.status, JSON.stringify(await submit.json()));
// 顺手验一下防重复:立刻再提一次,应该拿到同一条任务
const again = await (await fetch(`${API}/api/projects/${PROJECT}/submit-export/`, { method: "POST", headers: auth })).json();
console.log("再提一次(应复用同一任务):", again.id, again.status);
for (let i = 0; i < 200; i += 1) {
await new Promise((r) => setTimeout(r, 2000));
const res = await (await fetch(`${API}/api/projects/${PROJECT}/poll-export/`, { method: "POST", headers: auth })).json();
const secs = ((Date.now() - t0) / 1000).toFixed(0);
console.log(` ${secs}s · ${res.status} · ${res.progress}%`, res.error_message || "");
if (res.status === "succeeded") {
console.log("成片:", res.output_url);
const head = await fetch(res.output_url, { method: "HEAD" });
console.log("成片可访问:", head.status, head.headers.get("content-type"), head.headers.get("content-length"), "bytes");
const detail = await (await fetch(`${API}/api/projects/${PROJECT}/`, { headers: auth })).json();
console.log("详情 final_video_url:", detail.final_video_url ? "有" : "空", "· current_stage:", detail.current_stage);
break;
}
if (res.status === "failed") { console.log("失败:", res.error_message); break; }
}
+71
View File
@@ -0,0 +1,71 @@
// 抓图:视频阶段「合成完整视频 / 播放成片 / 下载成片」+ 项目列表播放键播成片
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = process.env.BASE || "http://127.0.0.1:5173";
const API = process.env.API || "http://127.0.0.1:8010";
const OUT = process.argv[2] || "shots-merge";
mkdirSync(OUT, { recursive: true });
const login = await (await fetch(`${API}/api/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "airshelf", password: "Restraint2026" })
})).json();
const tok = login.token;
const auth = { Authorization: `Token ${tok}`, "Content-Type": "application/json" };
// 挑一个「所有场次都已出片」的项目:合成按钮才是可点态
const projects = (await (await fetch(`${API}/api/projects/?page_size=100`, { headers: auth })).json()).results || [];
console.log("项目数:", projects.length, "· 已有成片:", projects.filter((p) => p.final_video_url).length);
let target = null;
for (const p of projects.filter((p) => p.status === "completed" || p.current_stage === "video")) {
const detail = await (await fetch(`${API}/api/projects/${p.id}/`, { headers: auth })).json();
const segs = detail.video_segments || [];
if (segs.length && segs.every((s) => s.status === "succeeded" && s.adopted_asset)) {
target = detail;
break;
}
}
if (!target) throw new Error("没有全部场次出片的项目,无法抓合成态");
console.log("target:", target.id, target.name, "· 场次:", target.video_segments.length, "· 成片:", target.final_video_url || "(未合成)");
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
await ctx.addInitScript((t) => localStorage.setItem("airshelf_token", t), tok);
const page = await ctx.newPage();
page.on("pageerror", (e) => console.error(" pageerror:", e.message));
await page.goto(`${BASE}/pipeline/${target.id}#stage-4`, { waitUntil: "networkidle", timeout: 40000 });
await page.waitForTimeout(2500);
const foot = page.locator('[data-stage-pane="4"] .stage-foot');
await foot.waitFor({ timeout: 15000 });
await page.screenshot({ path: `${OUT}/1-stage4-merge-bar.png`, clip: await foot.boundingBox() });
console.log("shot 1-stage4-merge-bar");
// 点「播放成片」→ 灯箱播成片
const play = page.locator('[data-stage-pane="4"] .stage-foot button', { hasText: "播放成片" });
if (await play.count()) {
await play.click();
await page.waitForTimeout(2500);
await page.screenshot({ path: `${OUT}/2-stage4-play-final.png` });
console.log("shot 2-stage4-play-final");
await page.keyboard.press("Escape");
}
await page.goto(`${BASE}/projects`, { waitUntil: "networkidle", timeout: 40000 });
await page.waitForTimeout(1500);
await page.screenshot({ path: `${OUT}/3-projects-list.png`, fullPage: false });
console.log("shot 3-projects-list");
// 列表行末播放键:有成片 → 直接弹窗播,不再跳流水线
const row = page.locator(`tr[data-name="${target.name}"] .row-action a`).first();
if (await row.count()) {
await row.click();
await page.waitForTimeout(2500);
await page.screenshot({ path: `${OUT}/4-projects-play-final.png` });
console.log("shot 4-projects-play-final · 当前地址:", page.url());
}
await browser.close();
console.log("DONE ->", OUT);
+93
View File
@@ -0,0 +1,93 @@
// 二期第7段验收抓图:5.1 存为模板入口 + 弹窗;5.2 新建项目选模板;5.3 同套路重出预期文案
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = process.env.BASE || "http://127.0.0.1:5174";
const API = process.env.API || "http://127.0.0.1:8011";
const OUT = process.argv[2] || "shots-seg7";
mkdirSync(OUT, { recursive: true });
const login = await (await fetch(`${API}/api/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "seg7", password: "Restraint2026" })
})).json();
const tok = login.token;
const auth = { Authorization: `Token ${tok}`, "Content-Type": "application/json" };
const projects = (await (await fetch(`${API}/api/projects/`, { headers: auth })).json()).results || [];
const project = projects.find((p) => p.name.includes("夜光面膜"));
console.log("project:", project.id, project.name);
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 960 }, deviceScaleFactor: 2 });
await ctx.addInitScript((t) => localStorage.setItem("airshelf_token", t), tok);
const page = await ctx.newPage();
page.on("pageerror", (e) => console.error(" pageerror:", e.message));
async function shot(name, locator) {
const clip = locator ? await locator.boundingBox() : undefined;
await page.screenshot({ path: `${OUT}/${name}.png`, ...(clip ? { clip } : { fullPage: false }) });
console.log("shot", name);
}
// ── 1 · 脚本页底栏「存为模板」入口 ──
await page.goto(`${BASE}/pipeline/${project.id}?st=1#stage-1`, { waitUntil: "networkidle", timeout: 40000 });
await page.waitForTimeout(2500);
const foot = page.locator(".stage-foot").first();
await foot.scrollIntoViewIfNeeded();
await page.waitForTimeout(400);
await shot("1-save-template-entry", foot);
// ── 2 · 存为模板弹窗:摊开「会存进模板的内容」 ──
await page.getByRole("button", { name: /存为模板/ }).click();
await page.waitForTimeout(700);
await shot("2-save-template-modal", page.locator(".modal").first());
const nameInput = page.locator(".modal .input").first();
await nameInput.fill(`口播 · 痛点前置 · 4 镜 #${Date.now() % 1000}`);
await page.waitForTimeout(200);
await page.getByRole("button", { name: /保存模板/ }).click();
await page.waitForTimeout(1500);
await shot("3-saved-toast");
const templates = (await (await fetch(`${API}/api/script-templates/`, { headers: auth })).json()).results || [];
console.log("templates:", templates.map((t) => `${t.name} / ${t.shot_count}镜 / ${t.total_duration}s`));
console.log("outline_text:\n" + (templates[0]?.outline_text || "(空)"));
// ── 4 · 新建项目 · 第 2 步选模板 + 预期说明 ──
await page.goto(`${BASE}/projects/new`, { waitUntil: "networkidle", timeout: 40000 });
await page.waitForTimeout(1800);
const cards = page.locator(".product-card");
const count = await cards.count();
for (let i = 0; i < count; i += 1) {
if ((await cards.nth(i).innerText()).includes("鎏金精华水")) { await cards.nth(i).click(); break; }
}
await page.waitForTimeout(500);
const select = page.locator(".wiz-pane select.input").first();
await select.selectOption({ index: 1 });
await page.waitForTimeout(600);
const pane = page.locator(".step-pane-wrap").last();
await pane.scrollIntoViewIfNeeded();
await page.waitForTimeout(400);
await shot("4-wizard-template-picked", pane);
// ── 5 · 用模板开新项目 → 脚本页设定卡已带模板参数 + 预期文案 ──
const created = await (await fetch(`${API}/api/projects/`, {
method: "POST", headers: auth,
body: JSON.stringify({
name: `鎏金精华水 · 同套路 · ${Date.now() % 10000}`,
product: (await (await fetch(`${API}/api/products/`, { headers: auth })).json()).results.find((p) => p.title === "鎏金精华水").id,
metadata: { wizard: { template_id: templates[0].id, selling_point_ids: [] } }
})
})).json();
console.log("new project wizard:", JSON.stringify(created.metadata.wizard, null, 2));
await page.goto(`${BASE}/pipeline/${created.id}?st=1#stage-1`, { waitUntil: "networkidle", timeout: 40000 });
await page.waitForTimeout(2500);
await page.locator('.chat-mode[data-mode="ai"]').click();
await page.waitForTimeout(900);
await shot("5-setup-card-with-template", page.locator(".chat-pane").first());
await browser.close();
console.log("done ->", OUT);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB