feat: add style kit production standards

This commit is contained in:
xz
2026-09-01 01:11:22 +08:00
parent 8caf947b80
commit a9c8eea271
13 changed files with 1391 additions and 13 deletions
+167
View File
@@ -0,0 +1,167 @@
import assert from "node:assert/strict";
import { rm } from "node:fs/promises";
import { resolve } from "node:path";
import { dbRun, withTransaction } from "../server/db.mjs";
const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
const projectRoot = resolve(import.meta.dirname, "..");
const organizationId = "org-studio-lab";
const workspaceId = "ws-local-aidrama";
const projectId = "thunder-mouth";
const northstarScope = {
"x-organization-id": "org-northstar",
"x-workspace-id": "ws-northstar-main",
"x-project-id": "northstar-pilot"
};
async function request(path, options = {}) {
const response = await fetch(`${api}${path}`, {
...options,
headers: { "content-type": "application/json", ...(options.headers || {}) }
});
const payload = await response.json().catch(() => ({}));
return { response, payload };
}
function expectOk(result, label) {
assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`);
return result.payload;
}
async function login(email) {
const result = expectOk(await request("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password: "Demo@123456" })
}), `${email} login`);
return { authorization: `Bearer ${result.session.token}` };
}
const ownerAuth = await login("producer@local.test");
const writerAuth = await login("writer@local.test");
const northstarAuth = await login("producer2@local.test");
const headers = { ...ownerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
const writerHeaders = { ...writerAuth, "x-organization-id": organizationId, "x-workspace-id": workspaceId, "x-project-id": projectId };
const northstarHeaders = { ...northstarAuth, ...northstarScope };
const createdStyleKitIds = [];
const createdJobIds = [];
try {
const initial = expectOk(await request("/api/style-kits", { headers }), "list seeded style kits");
assert.ok(initial.kits.some((kit) => kit.id === "stylekit-studio-guoman-vertical"), "seeded studio style kit must be visible");
assert.equal(initial.effective.styleKit.id, "stylekit-studio-guoman-vertical", "thunder-mouth must inherit seeded style kit binding");
assert.equal(initial.effective.binding.projectId, projectId, "effective binding must belong to current project");
assert.equal(initial.effective.styleKit.voicePolicy.forbidRandomNativeVoice, true, "fixed voice policy must forbid random native voice as final");
const writerRead = await request("/api/style-kits", { headers: writerHeaders });
assert.equal(writerRead.response.status, 200, "writer should read effective style kit for generation");
const writerCreate = await request("/api/style-kits", {
method: "POST",
headers: writerHeaders,
body: JSON.stringify({ name: "编剧不应创建", scopeMode: "workspace" })
});
assert.equal(writerCreate.response.status, 403, "writer must not manage style kits");
const styleKitId = `stylekit-smoke-${Date.now()}`;
const created = expectOk(await request("/api/style-kits", {
method: "POST",
headers,
body: JSON.stringify({
id: styleKitId,
name: "Smoke 商用风格标准",
scopeMode: "workspace",
status: "active",
description: "用于 smoke 验收的本地商用风格标准。",
visualProfile: {
styleFamily: "原创国漫 2D 动画",
cameraLanguage: "稳定中景,单一完整画面",
continuityLocks: ["character", "costume", "location", "prop", "weather", "camera"]
},
subtitlePreset: { language: "zh-CN", position: "bottom-safe-area", maxCharsPerLine: 17 },
voicePolicy: { finalVoiceMode: "fixed-reference-required", forbidRandomNativeVoice: true, fallbackMouthPlan: ["侧脸", "反应镜头"] },
deliverySpec: { aspectRatio: "9:16", resolution: "1080x1920", fps: 24, clipDurationSec: [5, 10], exportFormat: "mp4" },
promptRules: {
positivePrefix: "ONE SINGLE COMPLETE 9:16 CHINESE ANIMATION FRAME, smoke standard.",
negativeTerms: ["split-screen", "comic panel", "collage", "contact sheet", "storyboard", "多格", "拼图", "分屏", "watermark"]
},
singleFramePolicy: { enabled: true, imageOutputCount: 1, batchSize: 1, forbiddenTerms: ["split-screen", "collage", "contact sheet", "多格"] },
namingRules: { shotPattern: "{projectId}/smoke/shot-{shotNumber}-{version}", deliveryPattern: "{projectName}-smoke-{releaseVersion}" },
qaPolicy: { gates: ["single-frame", "continuity", "voice-rights", "subtitle-asr-alignment"], minimumReviewScore: 91, requireLockedAssets: true },
metadata: { source: "smoke-style-kits" }
})
}), "create style kit");
createdStyleKitIds.push(styleKitId);
assert.equal(created.styleKit.id, styleKitId, "created style kit id must be stable");
assert.ok(created.styleKit.promptRules.negativeTerms.includes("contact sheet"), "server must normalize single-frame negative terms");
assert.equal(created.styleKit.singleFramePolicy.batchSize, 1, "style kit must lock batch size to 1");
const bound = expectOk(await request(`/api/projects/${encodeURIComponent(projectId)}/style-kit`, {
method: "POST",
headers,
body: JSON.stringify({ styleKitId, enforcement: "strict", notes: "smoke binds the workspace kit to the current project" })
}), "bind style kit to project");
assert.equal(bound.effective.styleKit.id, styleKitId, "effective style kit must switch after binding");
assert.equal(bound.effective.binding.enforcement, "strict", "binding enforcement must be persisted");
const effective = expectOk(await request("/api/style-kits/effective", { headers }), "read effective style kit");
assert.equal(effective.styleKit.id, styleKitId, "effective endpoint must return project binding");
const preview = expectOk(await request("/api/jobs/preview", {
method: "POST",
headers,
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform" })
}), "preview generation job with style kit");
assert.equal(preview.preview.styleKit.id, styleKitId, "generation preview contract must include effective style kit");
assert.equal(preview.preview.constraints.imageOutputCount, 1, "generation contract must keep image output count at 1");
assert.equal(preview.preview.constraints.batchSize, 1, "generation contract must keep batch size at 1");
assert.ok(preview.preview.constraints.negativePromptTerms.includes("contact sheet"), "generation contract must inject single-frame negative terms");
assert.equal(preview.preview.constraints.voicePolicy.forbidRandomNativeVoice, true, "generation contract must carry final voice policy");
const blockedPrompt = await request("/api/jobs/preview", {
method: "POST",
headers,
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform", prompt: "请生成 split-screen 对比图" })
});
assert.equal(blockedPrompt.response.status, 422, "positive prompt with split-screen must be blocked");
assert.equal(blockedPrompt.payload.error, "single_frame_contract_violation", "single-frame violation error code must be explicit");
const crossOrgBind = await request(`/api/projects/${encodeURIComponent("northstar-pilot")}/style-kit`, {
method: "POST",
headers: northstarHeaders,
body: JSON.stringify({ styleKitId, enforcement: "strict" })
});
assert.equal(crossOrgBind.response.status, 404, "another organization must not bind this style kit");
const createdJob = expectOk(await request("/api/jobs", {
method: "POST",
headers,
body: JSON.stringify({ kind: "单画面关键帧", shotId: "shot-01", adapter: "owned-model-platform", output: `qa/style-kit/${styleKitId}/frame.json` })
}), "create generation job with style kit");
createdJobIds.push(createdJob.job.id);
assert.equal(createdJob.job.request.styleKit.id, styleKitId, "created job request_json must persist style kit");
assert.equal(createdJob.job.request.constraints.styleEnforcement, "project-binding", "created job must record style enforcement source");
console.log(`style kits smoke passed: ${styleKitId}`);
} finally {
await request(`/api/projects/${encodeURIComponent(projectId)}/style-kit`, {
method: "POST",
headers,
body: JSON.stringify({ styleKitId: "stylekit-studio-guoman-vertical", enforcement: "strict", notes: "《雷雨口》商用 MVP 默认继承工作区国漫竖屏生产标准。" })
}).catch(() => {});
withTransaction(() => {
for (const jobId of createdJobIds) {
dbRun("DELETE FROM job_dependencies WHERE job_id = ? OR depends_on_job_id = ?", [jobId, jobId]);
dbRun("DELETE FROM job_attempts WHERE job_id = ?", [jobId]);
dbRun("DELETE FROM usage_events WHERE metadata_json LIKE ?", [`%${jobId}%`]);
dbRun("DELETE FROM audit_logs WHERE target_type = 'generation_job' AND target_id = ?", [jobId]);
dbRun("DELETE FROM generation_jobs WHERE id = ?", [jobId]);
}
for (const styleKitId of createdStyleKitIds) {
dbRun("DELETE FROM project_style_kit_bindings WHERE style_kit_id = ?", [styleKitId]);
dbRun("DELETE FROM audit_logs WHERE target_type = 'style_kit' AND target_id = ?", [styleKitId]);
dbRun("DELETE FROM style_kits WHERE id = ?", [styleKitId]);
}
});
for (const jobId of createdJobIds) {
await rm(resolve(projectRoot, "storage", "jobs", jobId), { recursive: true, force: true });
}
}