From 8f80247e0deb5c9ef31475bde0dcc7c8e8357986 Mon Sep 17 00:00:00 2001 From: seaislee1209 Date: Fri, 5 Jun 2026 14:48:13 +0800 Subject: [PATCH] chore(core): idempotent demo seed (airshelf team) + data screenshot script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- core/backend/seed_demo.py | 105 ++++++++++++++++++++++++++++ core/qa/visual-parity/shot-data.mjs | 21 ++++++ 2 files changed, 126 insertions(+) create mode 100644 core/backend/seed_demo.py create mode 100644 core/qa/visual-parity/shot-data.mjs diff --git a/core/backend/seed_demo.py b/core/backend/seed_demo.py new file mode 100644 index 0000000..f0d01d0 --- /dev/null +++ b/core/backend/seed_demo.py @@ -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())) diff --git a/core/qa/visual-parity/shot-data.mjs b/core/qa/visual-parity/shot-data.mjs new file mode 100644 index 0000000..c188e4e --- /dev/null +++ b/core/qa/visual-parity/shot-data.mjs @@ -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);