chore(core): idempotent demo seed (airshelf team) + data screenshot script
- seed_demo.py: ORM-only demo data for airshelf team — 2 products (w/ images+selling points), 11 assets referencing real TOS objects (thumbnails resolve to real images), one COMPLETED project with full pipeline graph (script/base_assets/storyboard/video_segments/timeline) so pipeline stage2-5 has real data to render. Idempotent (skips if demo product exists). No schema changes. - shot-data.mjs: capture dashboard/products/library with seeded data via 127.0.0.1:5180 - verified: products page shows real cover images via TOS preview_url Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8bcf7615df
commit
8f80247e0d
105
core/backend/seed_demo.py
Normal file
105
core/backend/seed_demo.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# 一次性灌演示数据(airshelf 团队)· 幂等 · 仅 ORM 插行,不碰表结构
|
||||||
|
# 运行: ./.venv/Scripts/python.exe seed_demo.py
|
||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "airshelf.settings.development")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.common.api import get_current_team
|
||||||
|
from apps.accounts.models import User
|
||||||
|
from apps.assets.models import Asset, AssetFile
|
||||||
|
from apps.products.models import Product, ProductImage, ProductSellingPoint
|
||||||
|
from apps.projects.models import (
|
||||||
|
BaseAssetGroup, ExportJob, Project, ProjectStage, ScriptSegment, ScriptVersion,
|
||||||
|
StoryboardFrame, StoryboardVersion, SubtitleTrack, BgmTrack, Timeline, TimelineClip,
|
||||||
|
VideoSegment, VideoSegmentVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.get(username="airshelf")
|
||||||
|
team = get_current_team(user)
|
||||||
|
DEMO = "演示 · 透真玻尿酸补水面膜"
|
||||||
|
if Product.objects.filter(team=team, title=DEMO).exists():
|
||||||
|
print("ALREADY SEEDED — skip")
|
||||||
|
raise SystemExit
|
||||||
|
|
||||||
|
src = list(AssetFile.objects.exclude(object_key="").values_list("object_key", "bucket"))
|
||||||
|
if not src:
|
||||||
|
print("NO SOURCE TOS OBJECTS — abort")
|
||||||
|
raise SystemExit
|
||||||
|
ctr = {"i": 0}
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
def mkasset(name, category, atype="image", source=Asset.Source.AI_GENERATED):
|
||||||
|
a = Asset.objects.create(team=team, created_by=user, name=name, asset_type=atype, source=source, category=category)
|
||||||
|
ok, bk = src[ctr["i"] % len(src)]
|
||||||
|
ctr["i"] += 1
|
||||||
|
AssetFile.objects.create(asset=a, object_key=ok, bucket=bk, content_type="image/png", size_bytes=120000, is_primary=True)
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
prod_imgs = [mkasset("补水面膜 · 正面主图", "product_image", source=Asset.Source.UPLOAD),
|
||||||
|
mkasset("补水面膜 · 质地细节", "product_image", source=Asset.Source.UPLOAD)]
|
||||||
|
persons = [mkasset("模特 · 林夏(都市白领)", "person"), mkasset("模特 · 夜见(清新自然)", "person")]
|
||||||
|
scenes = [mkasset("场景 · 深夜办公桌", "scene"), mkasset("场景 · 暖光化妆台", "scene")]
|
||||||
|
clips = [mkasset("片段 · 场1 深夜办公桌", "video_clip"), mkasset("片段 · 场2 面膜特写", "video_clip"), mkasset("片段 · 场3 化妆台定格", "video_clip")]
|
||||||
|
final = mkasset("成片 · 补水面膜痛点种草", "final_video", atype="video")
|
||||||
|
bgm = mkasset("BGM · 轻电子节奏", "uncategorized", atype="audio")
|
||||||
|
|
||||||
|
p = Product.objects.create(
|
||||||
|
team=team, created_by=user, title=DEMO, brand="透真", category="美妆个护",
|
||||||
|
target_audience="22-32 岁女性 · 敏感肌 · 办公室通勤",
|
||||||
|
description="30g 玻尿酸大精华面膜,深夜急救补水,敏感肌可用。", cover_asset=prod_imgs[0],
|
||||||
|
)
|
||||||
|
for i, a in enumerate(prod_imgs):
|
||||||
|
ProductImage.objects.create(product=p, asset=a, sort_order=i, is_primary=(i == 0))
|
||||||
|
for i, (t, d) in enumerate([("玻尿酸双效保湿", "4 小时持久水润"), ("30g 大精华", "一片顶三片"), ("敏感肌可用", "无香精 · 无酒精")]):
|
||||||
|
ProductSellingPoint.objects.create(product=p, title=t, detail=d, sort_order=i)
|
||||||
|
|
||||||
|
Product.objects.create(team=team, created_by=user, title="演示 · 南卡 Lite Pro 蓝牙耳机", brand="南卡", category="数码 3C", description="主动降噪 · 35h 续航。")
|
||||||
|
|
||||||
|
proj = Project.objects.create(team=team, created_by=user, name="演示 · 补水面膜 · 痛点种草 v1", product=p, status=Project.Status.COMPLETED, current_stage="export")
|
||||||
|
for st in ["script", "base_assets", "storyboard", "video", "export"]:
|
||||||
|
ProjectStage.objects.create(project=proj, stage=st, status=ProjectStage.Status.SUCCEEDED, started_at=now, completed_at=now)
|
||||||
|
|
||||||
|
sv = ScriptVersion.objects.create(project=proj, title="痛点种草 v1", content="深夜办公→痛点→产品→使用→卖点收尾", source="ai", is_adopted=True)
|
||||||
|
segs = []
|
||||||
|
for i, (dur, nar, vis) in enumerate([
|
||||||
|
(15, "加班三天,脸已经不能看了…", "深夜办公桌,疲惫特写"),
|
||||||
|
(15, "还好我有这个透真玻尿酸面膜", "面膜包装特写"),
|
||||||
|
(15, "敷完起来脸是软的,化妆都服帖", "化妆台,产品定格"),
|
||||||
|
]):
|
||||||
|
segs.append(ScriptSegment.objects.create(script_version=sv, sort_order=i, duration_seconds=dur, narration=nar, visual_prompt=vis))
|
||||||
|
|
||||||
|
for kind, adopted, cands in [
|
||||||
|
(BaseAssetGroup.Kind.PRODUCT, prod_imgs[0], prod_imgs),
|
||||||
|
(BaseAssetGroup.Kind.PERSON, persons[0], persons),
|
||||||
|
(BaseAssetGroup.Kind.SCENE, scenes[0], scenes),
|
||||||
|
]:
|
||||||
|
g = BaseAssetGroup.objects.create(project=proj, kind=kind, prompt=f"{kind} 基础资产", adopted_asset=adopted, version=1)
|
||||||
|
g.candidate_assets.set(cands)
|
||||||
|
|
||||||
|
sb = StoryboardVersion.objects.create(project=proj, prompt="统一商品/人物/场景风格,生成可指导视频的分镜", is_adopted=True)
|
||||||
|
for i, (a, seg) in enumerate(zip([scenes[0], prod_imgs[1], scenes[1]], segs)):
|
||||||
|
StoryboardFrame.objects.create(storyboard=sb, script_segment=seg, asset=a, sort_order=i, prompt=seg.visual_prompt)
|
||||||
|
|
||||||
|
for i, (seg, clip) in enumerate(zip(segs, clips)):
|
||||||
|
vs = VideoSegment.objects.create(project=proj, script_segment=seg, sort_order=i, target_duration_seconds=15, status=VideoSegment.Status.SUCCEEDED)
|
||||||
|
vv = VideoSegmentVersion.objects.create(video_segment=vs, asset=clip, prompt=seg.visual_prompt, is_adopted=True)
|
||||||
|
vs.adopted_version = vv
|
||||||
|
vs.save(update_fields=["adopted_version"])
|
||||||
|
|
||||||
|
tl = Timeline.objects.create(project=proj, name="补水面膜成片", aspect_ratio="9:16", resolution="1080x1920", duration_seconds=45)
|
||||||
|
for i, clip in enumerate(clips):
|
||||||
|
TimelineClip.objects.create(timeline=tl, asset=clip, sort_order=i, start_ms=i * 15000, duration_ms=15000)
|
||||||
|
SubtitleTrack.objects.create(timeline=tl, content=[{"start_ms": i * 15000, "text": seg.narration} for i, seg in enumerate(segs)], enabled=True)
|
||||||
|
BgmTrack.objects.create(timeline=tl, asset=bgm, volume=60, start_ms=0)
|
||||||
|
ExportJob.objects.create(timeline=tl, status=ExportJob.Status.SUCCEEDED, output_asset=final, progress=100)
|
||||||
|
|
||||||
|
print("SEEDED ok | product=%s project=%s assets=%d products=%d" % (
|
||||||
|
p.id, proj.id, Asset.objects.filter(team=team).count(), Product.objects.filter(team=team).count()))
|
||||||
21
core/qa/visual-parity/shot-data.mjs
Normal file
21
core/qa/visual-parity/shot-data.mjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 抓图:灌库 + TOS 后,现有页面的真数据+真图效果
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
import { mkdirSync } from "node:fs";
|
||||||
|
const BASE = "http://127.0.0.1:5180";
|
||||||
|
const API = "http://127.0.0.1:8010";
|
||||||
|
const OUT = process.argv[2] || "shots-data";
|
||||||
|
mkdirSync(OUT, { recursive: true });
|
||||||
|
const token = (await (await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "airshelf", password: "Restraint2026" }) })).json()).token;
|
||||||
|
const pages = [["dashboard", "/dashboard"], ["products", "/products"], ["library", "/library"]];
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 });
|
||||||
|
await ctx.addInitScript((t) => localStorage.setItem("airshelf_token", t), token);
|
||||||
|
const page = await ctx.newPage();
|
||||||
|
for (const [name, route] of pages) {
|
||||||
|
await page.goto(BASE + route, { waitUntil: "networkidle", timeout: 30000 });
|
||||||
|
await page.waitForTimeout(1800);
|
||||||
|
await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true });
|
||||||
|
console.log("shot", name);
|
||||||
|
}
|
||||||
|
await browser.close();
|
||||||
|
console.log("DONE ->", OUT);
|
||||||
Loading…
x
Reference in New Issue
Block a user