feat: add child motion picture book stage tooling
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-10 23:10:24 +08:00
parent 85ed8ca90c
commit 5cc8293380
8 changed files with 609 additions and 149 deletions

View File

@@ -2897,6 +2897,86 @@ test('owned public puzzle detail edits original draft instead of remixing', asyn
expect(getPuzzleAgentSession).toHaveBeenCalledWith('puzzle-session-1');
expect(remixPuzzleGalleryWork).not.toHaveBeenCalled();
expect(await screen.findByText('拼图结果页')).toBeTruthy();
const generatingPuzzleDraft: PuzzleResultDraft = {
workTitle: '暖灯猫街作品',
workDescription: '一套雨夜猫街主题拼图。',
levelName: '雨夜猫街',
summary: '屋檐下的猫与暖灯街角。',
themeTags: ['猫咪', '雨夜', '暖灯'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack: {
themePromise: {
key: 'theme_promise',
label: '主题承诺',
value: '雨夜猫街',
status: 'confirmed',
},
visualSubject: {
key: 'visual_subject',
label: '视觉主体',
value: '屋檐下的猫',
status: 'confirmed',
},
visualMood: {
key: 'visual_mood',
label: '视觉气质',
value: '温暖',
status: 'confirmed',
},
compositionHooks: {
key: 'composition_hooks',
label: '构图钩子',
value: '雨滴与灯牌',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tags_and_forbidden',
label: '标签与禁区',
value: '猫咪、雨夜',
status: 'confirmed',
},
},
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/candidate-1.png',
assetId: 'asset-1',
prompt: '雨夜猫咪',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/candidate-1.png',
coverAssetId: 'asset-1',
generationStatus: 'generating',
levels: [
{
levelId: 'puzzle-level-1',
levelName: '雨夜猫街',
pictureDescription: '屋檐下的猫与暖灯街角。',
pictureReference: null,
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/candidate-1.png',
assetId: 'asset-1',
prompt: '雨夜猫咪',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/candidate-1.png',
coverAssetId: 'asset-1',
generationStatus: 'generating',
},
],
metadata: null,
};
vi.mocked(executePuzzleAgentAction).mockResolvedValueOnce({
operation: {
@@ -2944,92 +3024,13 @@ test('owned public puzzle detail edits original draft instead of remixing', asyn
status: 'confirmed',
},
},
draft: {
workTitle: '暖灯猫街作品',
workDescription: '一套雨夜猫街主题拼图。',
levelName: '雨夜猫街',
summary: '屋檐下的猫与暖灯街角。',
themeTags: ['猫咪', '雨夜', '暖灯'],
forbiddenDirectives: [],
creatorIntent: null,
anchorPack: {
themePromise: {
key: 'theme_promise',
label: '主题承诺',
value: '雨夜猫街',
status: 'confirmed',
},
visualSubject: {
key: 'visual_subject',
label: '视觉主体',
value: '屋檐下的猫',
status: 'confirmed',
},
visualMood: {
key: 'visual_mood',
label: '视觉气质',
value: '温暖',
status: 'confirmed',
},
compositionHooks: {
key: 'composition_hooks',
label: '构图钩子',
value: '雨滴与灯牌',
status: 'confirmed',
},
tagsAndForbidden: {
key: 'tags_and_forbidden',
label: '标签与禁区',
value: '猫咪、雨夜',
status: 'confirmed',
},
},
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/candidate-1.png',
assetId: 'asset-1',
prompt: '雨夜猫咪',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/candidate-1.png',
coverAssetId: 'asset-1',
generationStatus: 'generating',
levels: [
{
levelId: 'puzzle-level-1',
levelName: '雨夜猫街',
pictureDescription: '屋檐下的猫与暖灯街角。',
pictureReference: null,
candidates: [
{
candidateId: 'candidate-1',
imageSrc: '/puzzle/candidate-1.png',
assetId: 'asset-1',
prompt: '雨夜猫咪',
actualPrompt: null,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'candidate-1',
coverImageSrc: '/puzzle/candidate-1.png',
coverAssetId: 'asset-1',
generationStatus: 'generating',
},
],
metadata: null,
},
draft: generatingPuzzleDraft,
messages: [],
lastAssistantReply: '大鱼结果页草稿已经生成,可以补正式资产。',
publishedProfileId: null,
suggestedActions: [],
resultPreview: {
draft: null,
draft: generatingPuzzleDraft,
publishReady: false,
blockers: [],
qualityFindings: [],

View File

@@ -5696,13 +5696,18 @@ button {
}
.child-motion-demo {
--child-motion-bg: #07151c;
--child-motion-panel: rgba(6, 24, 30, 0.64);
--child-motion-panel-border: rgba(178, 239, 220, 0.25);
--child-motion-text: #eefcf7;
--child-motion-soft: rgba(238, 252, 247, 0.7);
--child-motion-green: #5ff08f;
--child-motion-sky: #8fd8ff;
--child-motion-bg: #dff1d6;
--child-motion-sky: #cfefff;
--child-motion-cloud: rgba(255, 255, 255, 0.82);
--child-motion-ground: #78b76a;
--child-motion-ground-deep: #3b7f46;
--child-motion-ground-shadow: rgba(56, 110, 60, 0.3);
--child-motion-panel: rgba(255, 250, 241, 0.76);
--child-motion-panel-border: rgba(98, 132, 88, 0.18);
--child-motion-text: #27412a;
--child-motion-soft: rgba(39, 65, 42, 0.74);
--child-motion-green: #70c16b;
--child-motion-sky-accent: #95d2ff;
display: grid;
width: 100%;
min-width: 0;
@@ -5711,9 +5716,9 @@ button {
place-items: center;
overflow: hidden;
background:
radial-gradient(circle at 18% 14%, rgba(143, 216, 255, 0.24), transparent 32%),
radial-gradient(circle at 82% 22%, rgba(95, 240, 143, 0.18), transparent 30%),
linear-gradient(180deg, #092433 0%, var(--child-motion-bg) 54%, #0a1f18 100%);
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.92), transparent 18%),
radial-gradient(circle at 82% 18%, rgba(255, 255, 255, 0.56), transparent 17%),
linear-gradient(180deg, #f8fcff 0%, #eaf7ff 26%, var(--child-motion-sky) 52%, #dcefd0 70%, #cde3bd 100%);
color: var(--child-motion-text);
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
}
@@ -5725,15 +5730,47 @@ button {
}
}
.child-motion-demo::before,
.child-motion-demo::after {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
content: '';
}
.child-motion-demo::before {
background:
radial-gradient(circle at 12% 16%, var(--child-motion-cloud) 0 3.4%, transparent 3.6%),
radial-gradient(circle at 16% 15%, rgba(255, 255, 255, 0.86) 0 2.2%, transparent 2.5%),
radial-gradient(circle at 17.8% 16.2%, rgba(255, 255, 255, 0.9) 0 2.7%, transparent 3%),
radial-gradient(circle at 76% 13%, var(--child-motion-cloud) 0 4.1%, transparent 4.3%),
radial-gradient(circle at 82% 12.6%, rgba(255, 255, 255, 0.88) 0 2.5%, transparent 2.8%),
radial-gradient(circle at 85% 14.2%, rgba(255, 255, 255, 0.82) 0 2.1%, transparent 2.4%),
linear-gradient(180deg, rgba(255, 255, 255, 0) 0 62%, rgba(255, 255, 255, 0.08) 100%);
opacity: 0.9;
}
.child-motion-demo::after {
background:
radial-gradient(ellipse at 50% 100%, rgba(61, 120, 76, 0.26) 0 32%, transparent 58%),
linear-gradient(180deg, transparent 0 58%, rgba(255, 255, 255, 0.12) 76%, transparent 100%);
mix-blend-mode: soft-light;
opacity: 0.68;
}
.child-motion-stage {
position: relative;
z-index: 1;
width: min(100vw, calc(100vh * 16 / 9));
height: min(100vh, calc(100vw * 9 / 16));
overflow: hidden;
background:
linear-gradient(180deg, rgba(16, 64, 86, 0.86), rgba(9, 42, 39, 0.9)),
var(--child-motion-bg);
box-shadow: 0 30px 100px rgba(0, 0, 0, 0.38);
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0)),
radial-gradient(circle at 50% 18%, rgba(255, 255, 255, 0.6), transparent 24%),
linear-gradient(180deg, #f3fbff 0%, #e4f3ff 32%, #d9efc4 56%, #bbdea1 100%);
box-shadow: 0 30px 100px rgba(62, 98, 53, 0.18);
isolation: isolate;
touch-action: none;
user-select: none;
}
@@ -5745,18 +5782,48 @@ button {
}
}
.child-motion-stage::before,
.child-motion-stage::after {
position: absolute;
inset: 0;
pointer-events: none;
content: '';
}
.child-motion-stage::before {
z-index: 0;
background-image: url('/child-motion-demo/picture-book-grass-stage.webp');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
opacity: 0.88;
filter: saturate(1.02) contrast(0.98) brightness(1.02);
}
.child-motion-stage::after {
z-index: 1;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, transparent 18%),
radial-gradient(ellipse at 50% 82%, rgba(255, 245, 220, 0.16), transparent 42%),
linear-gradient(180deg, transparent 0 58%, rgba(80, 141, 72, 0.14) 100%);
opacity: 0.95;
}
.child-motion-camera-layer {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
background:
radial-gradient(circle at 50% 33%, rgba(255, 255, 255, 0.12), transparent 28%),
linear-gradient(110deg, rgba(255, 255, 255, 0.06) 0 12%, transparent 12% 20%, rgba(255, 255, 255, 0.04) 20% 31%, transparent 31% 100%);
filter: blur(7px) saturate(0.8);
opacity: 0.62;
transform: scale(1.05);
linear-gradient(180deg, rgba(255, 255, 255, 0.58), rgba(255, 255, 255, 0.08)),
radial-gradient(circle at 50% 33%, rgba(255, 255, 255, 0.42), transparent 30%),
linear-gradient(120deg, rgba(255, 255, 255, 0.1) 0 11%, transparent 11% 20%, rgba(255, 255, 255, 0.08) 20% 30%, transparent 30% 100%);
filter: blur(8px) saturate(0.92);
opacity: 0.34;
transform: scale(1.04);
mix-blend-mode: soft-light;
}
.child-motion-camera-state {
@@ -5765,14 +5832,19 @@ button {
left: 50%;
z-index: 7;
transform: translateX(-50%);
border: 1px solid rgba(238, 252, 247, 0.2);
border: 1px solid rgba(103, 140, 94, 0.18);
border-radius: 999px;
background: rgba(6, 24, 30, 0.52);
color: rgba(238, 252, 247, 0.82);
background: rgba(255, 250, 241, 0.7);
color: rgba(39, 65, 42, 0.88);
padding: 0.45rem 0.9rem;
font-size: clamp(0.68rem, 1.35vw, 0.84rem);
font-weight: 800;
backdrop-filter: blur(12px);
box-shadow: 0 10px 28px rgba(90, 120, 82, 0.14);
}
.child-motion-camera-state--ready {
display: none;
}
.child-motion-floor {
@@ -5780,12 +5852,49 @@ button {
right: -8%;
bottom: -19%;
left: -8%;
z-index: 2;
height: 47%;
border-radius: 50% 50% 0 0;
background:
radial-gradient(ellipse at 50% 8%, rgba(190, 255, 220, 0.22), transparent 36%),
linear-gradient(180deg, rgba(24, 86, 67, 0.84), rgba(7, 43, 34, 0.96));
box-shadow: inset 0 22px 70px rgba(255, 255, 255, 0.07);
radial-gradient(ellipse at 50% 10%, rgba(255, 255, 255, 0.22), transparent 30%),
radial-gradient(ellipse at 42% 30%, rgba(255, 246, 205, 0.2) 0 8%, transparent 18%),
radial-gradient(ellipse at 70% 25%, rgba(255, 255, 255, 0.18) 0 5%, transparent 14%),
linear-gradient(180deg, rgba(135, 194, 104, 0.92), rgba(69, 145, 76, 0.98));
box-shadow:
inset 0 26px 70px rgba(255, 255, 255, 0.16),
inset 0 -38px 68px rgba(52, 94, 46, 0.18);
}
.child-motion-floor::before,
.child-motion-floor::after {
position: absolute;
border-radius: 999px;
content: '';
}
.child-motion-floor::before {
inset: 14% 10% auto 16%;
height: 18%;
background:
radial-gradient(circle at 8% 50%, rgba(96, 148, 60, 0.68) 0 12%, transparent 13%),
radial-gradient(circle at 21% 42%, rgba(96, 148, 60, 0.58) 0 9%, transparent 10%),
radial-gradient(circle at 33% 55%, rgba(255, 255, 255, 0.2) 0 7%, transparent 8%),
radial-gradient(circle at 45% 40%, rgba(96, 148, 60, 0.62) 0 11%, transparent 12%),
radial-gradient(circle at 58% 52%, rgba(255, 255, 255, 0.16) 0 6%, transparent 7%),
radial-gradient(circle at 69% 42%, rgba(96, 148, 60, 0.62) 0 10%, transparent 11%),
radial-gradient(circle at 82% 50%, rgba(255, 255, 255, 0.18) 0 7%, transparent 8%);
opacity: 0.78;
}
.child-motion-floor::after {
inset: auto 6% 10%;
height: 15%;
background:
radial-gradient(circle at 18% 50%, rgba(55, 104, 53, 0.42) 0 10%, transparent 11%),
radial-gradient(circle at 38% 50%, rgba(255, 255, 255, 0.12) 0 6%, transparent 7%),
radial-gradient(circle at 60% 48%, rgba(55, 104, 53, 0.38) 0 11%, transparent 12%),
radial-gradient(circle at 80% 52%, rgba(255, 255, 255, 0.1) 0 5%, transparent 6%);
opacity: 0.68;
}
.child-motion-hud {
@@ -5797,7 +5906,7 @@ button {
border: 1px solid var(--child-motion-panel-border);
border-radius: clamp(0.75rem, 2vw, 1.25rem);
background: var(--child-motion-panel);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.2);
box-shadow: 0 18px 48px rgba(72, 112, 68, 0.12);
backdrop-filter: blur(14px);
}
@@ -5834,12 +5943,13 @@ button {
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid rgba(238, 252, 247, 0.2);
border: 1px solid rgba(112, 143, 97, 0.2);
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(242, 248, 236, 0.92));
color: var(--child-motion-text);
font-size: clamp(0.72rem, 1.45vw, 0.95rem);
font-weight: 900;
box-shadow: 0 8px 20px rgba(96, 132, 82, 0.12);
}
.child-motion-ring {
@@ -5848,24 +5958,28 @@ button {
z-index: 3;
width: clamp(5.8rem, 13vw, 9rem);
aspect-ratio: 1;
transform: translateX(-50%) rotateX(62deg);
transform: translateX(-50%) rotateX(66deg);
border-radius: 999px;
background:
conic-gradient(
from -90deg,
rgba(255, 255, 255, 0.95) 0 var(--child-motion-ring-progress),
rgba(95, 240, 143, 0.18) var(--child-motion-ring-progress) 360deg
rgba(255, 255, 255, 0.88) 0 var(--child-motion-ring-progress),
rgba(102, 190, 95, 0.22) var(--child-motion-ring-progress) 360deg
);
box-shadow:
0 0 28px rgba(95, 240, 143, 0.42),
inset 0 0 26px rgba(255, 255, 255, 0.18);
0 0 18px rgba(120, 191, 110, 0.34),
0 0 0 6px rgba(255, 255, 255, 0.12),
inset 0 0 24px rgba(255, 255, 255, 0.2);
}
.child-motion-ring::before {
position: absolute;
inset: 14%;
border-radius: inherit;
background: rgba(8, 44, 36, 0.94);
background:
radial-gradient(circle at 50% 45%, rgba(255, 255, 255, 0.1), transparent 40%),
linear-gradient(180deg, rgba(151, 215, 139, 0.82), rgba(73, 151, 74, 0.94));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.38);
content: '';
}
@@ -5873,8 +5987,9 @@ button {
position: absolute;
inset: 34%;
border-radius: 999px;
background: var(--child-motion-green);
opacity: 0.28;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(150, 231, 137, 0.86));
opacity: 0.62;
box-shadow: 0 0 22px rgba(124, 199, 112, 0.44);
}
.child-motion-ring--active {
@@ -5887,7 +6002,7 @@ button {
}
to {
filter: brightness(1.25);
filter: brightness(1.08);
}
}
@@ -5899,6 +6014,7 @@ button {
height: clamp(6rem, 13vw, 10rem);
transform: translateX(-50%);
transition: left 260ms ease, transform 220ms ease;
filter: drop-shadow(0 6px 14px rgba(56, 92, 55, 0.18));
}
.child-motion-avatar--jumping {
@@ -5911,8 +6027,13 @@ button {
.child-motion-avatar__leg {
position: absolute;
display: block;
background: rgba(7, 18, 24, 0.82);
box-shadow: 0 0 24px rgba(143, 216, 255, 0.18);
background:
linear-gradient(180deg, rgba(77, 109, 79, 0.44), rgba(41, 65, 44, 0.7)),
rgba(245, 250, 245, 0.1);
opacity: 0.6;
border: 1px solid rgba(239, 249, 235, 0.18);
box-shadow: 0 0 24px rgba(143, 216, 255, 0.12);
backdrop-filter: blur(1px);
}
.child-motion-avatar__head {
@@ -5985,12 +6106,13 @@ button {
transform: translate(-50%, -50%);
align-items: center;
justify-content: center;
border: 2px solid rgba(95, 240, 143, 0.64);
border: 2px solid rgba(117, 186, 92, 0.56);
border-radius: 999px;
background: rgba(95, 240, 143, 0.1);
background: rgba(247, 251, 243, 0.18);
color: var(--child-motion-text);
font-size: clamp(1rem, 2.4vw, 1.55rem);
font-weight: 900;
box-shadow: 0 8px 24px rgba(79, 126, 67, 0.12);
}
.child-motion-gesture-guide__hand {
@@ -5998,7 +6120,7 @@ button {
top: 28%;
width: clamp(4rem, 9vw, 7rem);
aspect-ratio: 1;
border: 2px dashed rgba(95, 240, 143, 0.58);
border: 2px dashed rgba(117, 186, 92, 0.5);
border-radius: 999px;
animation: child-motion-hand-guide 1.1s ease-in-out infinite alternate;
}
@@ -6027,8 +6149,8 @@ button {
height: 0.8rem;
transform: translate(-50%, -50%);
border-radius: 999px;
background: #b9ffd0;
box-shadow: 0 0 16px rgba(95, 240, 143, 0.56);
background: #f6fff1;
box-shadow: 0 0 16px rgba(119, 194, 111, 0.56);
}
.child-motion-floating-reward {
@@ -6040,7 +6162,7 @@ button {
color: #ffffff;
font-size: clamp(1.4rem, 4vw, 2.4rem);
font-weight: 900;
text-shadow: 0 4px 26px rgba(0, 0, 0, 0.42);
text-shadow: 0 4px 26px rgba(61, 90, 54, 0.42);
animation: child-motion-reward-rise 0.72s ease-out forwards;
}
@@ -6070,6 +6192,7 @@ button {
background: var(--child-motion-panel);
padding: 0.45rem;
backdrop-filter: blur(14px);
box-shadow: 0 14px 32px rgba(82, 124, 72, 0.1);
}
.child-motion-calibration div {
@@ -6078,7 +6201,7 @@ button {
gap: 0.08rem;
justify-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.48);
padding: 0.36rem 0.55rem;
}
@@ -6103,11 +6226,11 @@ button {
transform: translate(-50%, -50%);
align-items: center;
gap: 0.85rem;
border: 1px solid rgba(178, 239, 220, 0.32);
border: 1px solid rgba(143, 176, 124, 0.24);
border-radius: 1.4rem;
background: rgba(6, 24, 30, 0.7);
background: rgba(255, 250, 241, 0.76);
padding: clamp(0.85rem, 2vw, 1.15rem);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.28);
box-shadow: 0 24px 70px rgba(82, 124, 72, 0.18);
backdrop-filter: blur(14px);
}
@@ -6116,12 +6239,12 @@ button {
min-height: clamp(3rem, 7vw, 4.2rem);
border: 0;
border-radius: 999px;
background: linear-gradient(135deg, #5ff08f, #8fd8ff);
color: #062018;
background: linear-gradient(135deg, #88cf74, #9dd3ff);
color: #214228;
font-size: clamp(1rem, 2.5vw, 1.4rem);
font-weight: 950;
cursor: pointer;
box-shadow: 0 16px 44px rgba(95, 240, 143, 0.28);
box-shadow: 0 16px 44px rgba(124, 182, 98, 0.24);
}
.child-motion-start-panel span {
@@ -6136,7 +6259,9 @@ button {
z-index: 30;
display: none;
place-items: center;
background: #07151c;
background:
radial-gradient(circle at 24% 22%, rgba(255, 255, 255, 0.88), transparent 20%),
linear-gradient(180deg, #f7fcff 0%, #dff3ff 54%, #c9e6b9 100%);
color: var(--child-motion-text);
font-size: 1.25rem;
font-weight: 900;

View File

@@ -78,4 +78,47 @@ describe('parseMocapPacket', () => {
expect.objectContaining({x: 0.9, y: 0.8, source: 'direct'}),
);
});
test('解析 mocap frame 的身体中心和左右手来源', () => {
const command = parseMocapPacket({
schema_version: '1.0',
stream: { type: 'mocap.frame' },
general: {
body: {
center_norm: [0.34, 0.58],
},
},
actions: [{ gesture: 'wave-left-hand' }],
hands: [
{
label: 'Left',
state: 'open_palm',
landmarks: [
{ name: 'wrist', x: 0.21, y: 0.31 },
{ name: 'index_mcp', x: 0.25, y: 0.33 },
{ name: 'middle_mcp', x: 0.27, y: 0.34 },
{ name: 'ring_mcp', x: 0.28, y: 0.35 },
{ name: 'pinky_mcp', x: 0.29, y: 0.36 },
],
},
{
label: 'Right',
state: 'unknown',
x: 0.72,
y: 0.32,
},
],
});
expect(command.bodyCenter).toEqual({x: 0.34, y: 0.58});
expect(command.actions).toEqual(
expect.arrayContaining(['wave_left_hand', 'open_palm']),
);
expect(command.leftHand).toEqual(
expect.objectContaining({side: 'left', source: 'palm_center'}),
);
expect(command.rightHand).toEqual(
expect.objectContaining({x: 0.72, y: 0.32, side: 'right'}),
);
});
});