前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、 迁移脚本、测试与自托管 docker-compose 示例。 开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读 publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为 VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注 Apache-2.0。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2257 lines
80 KiB
TypeScript
2257 lines
80 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
TK_PROMPT_HOOK_IDS,
|
||
TK_PROMPT_HOOK_PATTERNS,
|
||
TK_PROMPT_LANGUAGES,
|
||
TK_PROMPT_LIVE_PRESENTER_BOUNDARY,
|
||
TK_PROMPT_NON_LIVE_PERSON_BOUNDARY,
|
||
TK_PROMPT_STYLES,
|
||
TK_PROMPT_VISUAL_TEXT_BOUNDARY,
|
||
TK_PROMPT_VIDEO_MODES,
|
||
compileTkPromptCandidateBeats,
|
||
deriveTkPromptEvidenceFlags,
|
||
eligibleTkPromptHookIds,
|
||
isTkPromptChineseNarrative,
|
||
isTkPromptTargetLanguageText,
|
||
normalizeTkPromptCandidateBeatInformation,
|
||
normalizeTkPromptCandidateCameraMovements,
|
||
normalizeTkPromptCandidateFreeText,
|
||
normalizeTkPromptCandidateStructure,
|
||
normalizeTkPromptStrategyPromotions,
|
||
normalizeTkPromptStrategyAppearanceMode,
|
||
normalizeTkPromptStrategyDiversity,
|
||
renderTkPromptExecutionPrompt,
|
||
tkPromptEngineRequestSchema,
|
||
validateTkPromptOptionCandidate,
|
||
validateTkPromptFinalExecutionPrompt,
|
||
validateTkPromptStrategySet,
|
||
type TkPromptFactPack,
|
||
type TkPromptOptionCandidate,
|
||
type TkPromptStrategy,
|
||
} from '../src/domain/tkPromptEngine.js';
|
||
|
||
const ids = {
|
||
image: '00000000-0000-4000-8000-000000000101',
|
||
image2: '00000000-0000-4000-8000-000000000103',
|
||
image3: '00000000-0000-4000-8000-000000000104',
|
||
};
|
||
|
||
describe('TK Prompt Engine 1.0 冻结合同', () => {
|
||
it('精确冻结 10 个 Style、16 个 Hook 和 3 个 Video Mode', () => {
|
||
expect(TK_PROMPT_STYLES).toEqual([
|
||
'drama',
|
||
'reversal',
|
||
'interview',
|
||
'story',
|
||
'unboxing',
|
||
'product_pov',
|
||
'comparison',
|
||
'talking_head',
|
||
'pain_point',
|
||
'scene',
|
||
]);
|
||
expect(TK_PROMPT_HOOK_IDS).toEqual([
|
||
'visual_shock',
|
||
'suspense_question',
|
||
'contrast',
|
||
'pain_strike',
|
||
'before_after',
|
||
'sound_hook',
|
||
'challenge_doubt',
|
||
'identity',
|
||
'number_benefit',
|
||
'unexpected',
|
||
'confession',
|
||
'honest_flaw',
|
||
'elimination_haul',
|
||
'cost_math',
|
||
'comment_reply',
|
||
'hype_confirm',
|
||
]);
|
||
expect(TK_PROMPT_HOOK_PATTERNS).toHaveLength(16);
|
||
expect(new Set(TK_PROMPT_HOOK_PATTERNS.map(item => item.internalExample)).size).toBe(16);
|
||
expect(TK_PROMPT_VIDEO_MODES).toEqual([
|
||
'product_closeup',
|
||
'scene_demo',
|
||
'live_presenter',
|
||
]);
|
||
expect(TK_PROMPT_VIDEO_MODES).not.toContain('graphic_montage');
|
||
});
|
||
|
||
it('5 秒只允许纯声音且图片硬上限是 9', () => {
|
||
expect(tkPromptEngineRequestSchema.safeParse({
|
||
...baseRequest(),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image, ids.image2],
|
||
}).success).toBe(true);
|
||
expect(tkPromptEngineRequestSchema.safeParse({
|
||
...baseRequest(),
|
||
durationSeconds: 5,
|
||
spokenLanguage: '英语',
|
||
}).success).toBe(false);
|
||
expect(tkPromptEngineRequestSchema.safeParse({
|
||
...baseRequest(),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
verifiedOffer: { condition: '折扣10%' },
|
||
}).success).toBe(false);
|
||
expect(tkPromptEngineRequestSchema.safeParse({
|
||
...baseRequest(),
|
||
imageAssetIds: Array.from({ length: 10 }, (_, index) => (
|
||
`00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}`
|
||
)),
|
||
}).success).toBe(false);
|
||
expect(tkPromptEngineRequestSchema.parse(baseRequest()))
|
||
.not.toHaveProperty('authorizedActorAssetId');
|
||
expect(tkPromptEngineRequestSchema.safeParse({
|
||
...baseRequest(),
|
||
authorizedActorAssetId: '00000000-0000-4000-8000-000000000102',
|
||
}).success).toBe(false);
|
||
});
|
||
|
||
it('Hook 资格在策略前进行硬排除', () => {
|
||
const eligible = eligibleTkPromptHookIds({
|
||
durationSeconds: 5,
|
||
evidenceFlags: evidenceFlags(),
|
||
});
|
||
expect(eligible).toContain('visual_shock');
|
||
expect(eligible).toContain('sound_hook');
|
||
expect(eligible).not.toContain('suspense_question');
|
||
expect(eligible).not.toContain('before_after');
|
||
expect(eligible).not.toContain('number_benefit');
|
||
expect(eligible).not.toContain('comment_reply');
|
||
});
|
||
|
||
it('场景/素材自由文本不能升级为 Hook 事实证据', () => {
|
||
const flags = deriveTkPromptEvidenceFlags({
|
||
userConfirmedFacts: [],
|
||
verifiedOffer: '',
|
||
availableScenes: ['前后对比场景'],
|
||
availableAssets: ['真实评论、竞品和未拆封包装'],
|
||
});
|
||
expect(flags).toEqual(evidenceFlags());
|
||
expect(eligibleTkPromptHookIds({
|
||
durationSeconds: 10,
|
||
evidenceFlags: flags,
|
||
})).not.toContain('comment_reply');
|
||
|
||
expect(deriveTkPromptEvidenceFlags({
|
||
userConfirmedFacts: ['用户确认这是真实评论问题'],
|
||
verifiedOffer: '已验证折扣 10%',
|
||
})).toMatchObject({ realComment: true, numeric: true });
|
||
});
|
||
|
||
it('Hook 需要的证据事实必须与当前策略 allowedFactIds 相交', () => {
|
||
const factPack = makeFactPack({
|
||
facts: [...makeFactPack().facts, {
|
||
factId: 'confirmed-003',
|
||
status: 'user-confirmed',
|
||
text: '用户已提供真实评论原文问题',
|
||
sourceImageIds: [],
|
||
}],
|
||
});
|
||
const strategies = makeStrategies();
|
||
strategies[0] = { ...strategies[0]!, hookPatternId: 'comment_reply' };
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), realComment: true, sealedPackage: true },
|
||
})).toThrow(/证据事实/u);
|
||
strategies[0] = {
|
||
...strategies[0]!,
|
||
allowedFactIds: [...strategies[0]!.allowedFactIds, 'confirmed-003'],
|
||
};
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), realComment: true, sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
});
|
||
|
||
it('三案每对六维差异至少四项,live_presenter 无需演员素材即可参与', () => {
|
||
const factPack = makeFactPack();
|
||
const valid = makeStrategies();
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies: valid },
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: {
|
||
strategies: valid.map((strategy, index) => index === 0
|
||
? { ...strategy, styleForm: 'presenter', style: 'talking_head', videoMode: 'live_presenter' }
|
||
: strategy),
|
||
},
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})[0]).toMatchObject({ style: 'talking_head', videoMode: 'live_presenter' });
|
||
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: {
|
||
strategies: valid.map((strategy, index) => index === 0
|
||
? { ...strategy, styleForm: 'drama', style: 'drama', videoMode: 'scene_demo' }
|
||
: strategy),
|
||
},
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})[0]).toMatchObject({ style: 'drama', videoMode: 'scene_demo' });
|
||
});
|
||
|
||
it.each(TK_PROMPT_LANGUAGES)('%s 的英文漂移会在生产侧收口为所选目标语言', language => {
|
||
const candidate = makeCandidate(10);
|
||
const driftText = language === '英语'
|
||
? 'Mira los detalles del producto en acción.'
|
||
: 'See the product details in action.';
|
||
candidate.beats.forEach(beat => {
|
||
beat.voiceoverTarget = driftText;
|
||
beat.voiceoverZh = language === '普通话' ? '' : '查看商品细节与操作方式。';
|
||
});
|
||
const factPack = makeFactPack({
|
||
spokenLanguage: language,
|
||
});
|
||
const normalized = normalizeTkPromptCandidateFreeText({
|
||
candidate,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: language,
|
||
});
|
||
const allowed = [factPack.productName, factPack.category, ...factPack.facts.map(fact => fact.text)];
|
||
expect(normalized.beats.every(beat => (
|
||
isTkPromptTargetLanguageText(beat.voiceoverTarget, language, allowed)
|
||
))).toBe(true);
|
||
|
||
});
|
||
|
||
it('服务端确定性锁定三策略槽多样性,保留 Provider Hook 与出镜模式', () => {
|
||
const collapsed = makeStrategies().map(strategy => ({
|
||
...strategy,
|
||
styleForm: 'scene' as const,
|
||
style: 'scene' as const,
|
||
firstFrameId: 'pain_reveal' as const,
|
||
buyingMotivationId: 'problem_relief' as const,
|
||
proofMethodId: 'visible_operation' as const,
|
||
videoMode: 'live_presenter' as const,
|
||
}));
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: collapsed },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
requireLivePresenter: true,
|
||
})).toThrow(/DIVERSITY|差异/u);
|
||
|
||
const normalized = normalizeTkPromptStrategyDiversity({
|
||
strategySet: { strategies: collapsed },
|
||
});
|
||
expect(normalized.strategies.map(strategy => strategy.hookPatternId)).toEqual(
|
||
collapsed.map(strategy => strategy.hookPatternId),
|
||
);
|
||
expect(normalized.strategies.every(strategy => (
|
||
strategy.videoMode === 'live_presenter'
|
||
))).toBe(true);
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: normalized,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
requireLivePresenter: true,
|
||
})).toHaveLength(3);
|
||
});
|
||
|
||
it('显式选择人物出镜后允许 1/2/3 案使用 live_presenter,但至少使用一案', () => {
|
||
const base = makeStrategies();
|
||
for (const liveCount of [1, 2, 3]) {
|
||
const strategies = base.map((strategy, index) => index < liveCount
|
||
? {
|
||
...strategy,
|
||
videoMode: 'live_presenter' as const,
|
||
}
|
||
: strategy);
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
requireLivePresenter: true,
|
||
}).filter(strategy => strategy.videoMode === 'live_presenter')).toHaveLength(liveCount);
|
||
}
|
||
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: base },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
requireLivePresenter: true,
|
||
})).toThrow(/至少一案|人物/u);
|
||
});
|
||
|
||
it('显式非主持人出镜方式由服务端锁定,主持人专属风格确定性回落', () => {
|
||
const strategies = makeStrategies();
|
||
strategies[0] = {
|
||
...strategies[0]!,
|
||
styleForm: 'presenter',
|
||
style: 'talking_head',
|
||
videoMode: 'live_presenter',
|
||
proofMethodId: 'presenter_delivery',
|
||
};
|
||
strategies[1] = { ...strategies[1]!, videoMode: 'scene_demo' };
|
||
const normalized = normalizeTkPromptStrategyAppearanceMode({
|
||
strategySet: { strategies },
|
||
appearanceMode: 'product_closeup',
|
||
durationSeconds: 10,
|
||
});
|
||
expect(normalized.strategies.every(strategy => (
|
||
strategy.videoMode === 'product_closeup'
|
||
))).toBe(true);
|
||
expect(normalized.strategies[0]).toMatchObject({
|
||
styleForm: 'scene',
|
||
style: 'scene',
|
||
proofMethodId: 'visible_operation',
|
||
});
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: normalized,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
|
||
const mismatched = makeStrategies();
|
||
mismatched[0] = {
|
||
...mismatched[0]!,
|
||
slot: 'creative',
|
||
objective: 'attention',
|
||
styleForm: 'drama',
|
||
style: 'scene',
|
||
};
|
||
const structurallyLocked = normalizeTkPromptStrategyAppearanceMode({
|
||
strategySet: { strategies: mismatched },
|
||
appearanceMode: 'auto',
|
||
durationSeconds: 10,
|
||
});
|
||
expect(structurallyLocked.strategies[0]).toMatchObject({
|
||
slot: 'safe',
|
||
objective: 'conversion',
|
||
styleForm: 'scene',
|
||
style: 'scene',
|
||
});
|
||
});
|
||
|
||
it('主持人风格/证明只能使用 live_presenter,策略不得携带旧演员 ID', () => {
|
||
const talkingHead = makeStrategies();
|
||
talkingHead[0] = {
|
||
...talkingHead[0]!,
|
||
styleForm: 'presenter',
|
||
style: 'talking_head',
|
||
videoMode: 'scene_demo',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: talkingHead },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/live_presenter|真人/u);
|
||
|
||
const presenterProof = makeStrategies();
|
||
presenterProof[0] = {
|
||
...presenterProof[0]!,
|
||
proofMethodId: 'presenter_delivery',
|
||
proofMethod: '主持人现场演示',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: presenterProof },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/live_presenter|真人/u);
|
||
|
||
const actorOnProduct = makeStrategies().map((strategy, index) => index === 0
|
||
? {
|
||
...strategy,
|
||
authorizedActorAssetId: '00000000-0000-4000-8000-000000000102',
|
||
}
|
||
: strategy);
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: actorOnProduct },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow();
|
||
});
|
||
|
||
it('5 秒策略 openingLine 必须为空', () => {
|
||
const withSpeech = makeStrategies();
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: withSpeech },
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/openingLine|纯声音/u);
|
||
|
||
const soundOnly = withSpeech.map(strategy => ({ ...strategy, openingLine: '' }));
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies: soundOnly },
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
});
|
||
|
||
it('Hook 内部案例不得泄漏到策略或方案', () => {
|
||
const strategies = makeStrategies();
|
||
strategies[0] = {
|
||
...strategies[0]!,
|
||
productBridge: TK_PROMPT_HOOK_PATTERNS[0]!.internalExample,
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/Hook/u);
|
||
|
||
const normalizedLeak = makeStrategies();
|
||
normalizedLeak[0] = {
|
||
...normalizedLeak[0]!,
|
||
productBridge: TK_PROMPT_HOOK_PATTERNS[0]!.internalExample.split('').join(' '),
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: normalizedLeak },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/Hook/u);
|
||
|
||
for (const leakedHistory of [
|
||
'作为累计十亿 GMV 的顶级专家给出背书',
|
||
'拥有10年电商经验的专家推荐',
|
||
'创造十亿销售额的顶级策略师',
|
||
'world-class e-commerce expert with 10 years of experience and billions in sales',
|
||
]) {
|
||
const personaLeak = makeStrategies();
|
||
personaLeak[0] = { ...personaLeak[0]!, productBridge: leakedHistory };
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: personaLeak },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/履历|专家/u);
|
||
}
|
||
|
||
const ordinaryProductAge = makeStrategies();
|
||
ordinaryProductAge[0] = { ...ordinaryProductAge[0]!, productBridge: '商品标注保质期10年' };
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies: ordinaryProductAge },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
|
||
for (const disclosure of ['#AIGC', 'AI创作', '人工智能生成', 'AI-generated']) {
|
||
const aiLeak = makeStrategies();
|
||
aiLeak[0] = { ...aiLeak[0]!, productBridge: disclosure };
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: aiLeak },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/AI/u);
|
||
}
|
||
|
||
for (const disclosure of [
|
||
'#AIGC', 'AIGC', 'AI制作', 'AI创作', '人工智能生成',
|
||
'AI-generated content', 'created by AI',
|
||
]) {
|
||
const candidate = makeCandidate(10);
|
||
candidate.whyItFits = disclosure;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/AI/u);
|
||
}
|
||
const ordinaryModelName = makeCandidate(10);
|
||
ordinaryModelName.whyItFits = 'AIGCLEAN-200 product detail';
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: ordinaryModelName,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
|
||
const candidatePersonaLeak = makeCandidate(10);
|
||
candidatePersonaLeak.whyItFits = 'world-class e-commerce expert with 10 years of experience';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: candidatePersonaLeak,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/履历|专家/u);
|
||
});
|
||
|
||
it('证明方法不能绕过竞品与前后素材证据门禁', () => {
|
||
const comparison = makeStrategies();
|
||
comparison[0] = {
|
||
...comparison[0]!,
|
||
proofMethodId: 'verified_comparison',
|
||
proofMethod: '使用已确认竞品做同条件对比',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: comparison },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/竞品|对比/u);
|
||
comparison[0] = {
|
||
...comparison[0]!,
|
||
allowedFactIds: [...comparison[0]!.allowedFactIds, 'confirmed-003'],
|
||
};
|
||
const competitorFactPack = makeFactPack({
|
||
facts: [...makeFactPack().facts, {
|
||
factId: 'confirmed-003',
|
||
status: 'user-confirmed',
|
||
text: '用户确认已提供竞品对比素材',
|
||
sourceImageIds: [],
|
||
}],
|
||
});
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: { strategies: comparison },
|
||
factPack: competitorFactPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true, competitor: true },
|
||
})).toHaveLength(3);
|
||
|
||
const beforeAfter = makeStrategies();
|
||
beforeAfter[0] = {
|
||
...beforeAfter[0]!,
|
||
proofMethodId: 'verified_before_after',
|
||
proofMethod: '使用同场景前后素材证明',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: beforeAfter },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/前后/u);
|
||
});
|
||
|
||
it('无真实优惠时拒绝促销 CTA/发布文案,并零泄漏 forbiddenClaims', () => {
|
||
const promotionalStrategies = makeStrategies();
|
||
promotionalStrategies[0] = {
|
||
...promotionalStrategies[0]!,
|
||
ctaStrategy: '限时折扣,仅剩3件',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies: promotionalStrategies },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/未验证|价格|折扣|库存|限时/u);
|
||
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(10);
|
||
candidate.whyItFits = 'Limited time discount — only 3 left.';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/未验证|折扣|库存|限时/u);
|
||
|
||
candidate.whyItFits = 'A closer look at the product in action.';
|
||
candidate.beats[0]!.environment = '限时折扣活动现场';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/未验证|折扣|限时/u);
|
||
|
||
candidate.beats[0]!.environment = '真实家居台面';
|
||
candidate.beats[0]!.voiceoverZh = '保证有效';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ forbiddenClaims: ['保证有效'] }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/禁止声明/u);
|
||
|
||
const verifiedOfferPack = makeFactPack({
|
||
verifiedOffer: { condition: '折扣10%' },
|
||
});
|
||
const verifiedCandidate = makeCandidate(10);
|
||
verifiedCandidate.whyItFits = '折扣10%';
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: verifiedCandidate,
|
||
strategy,
|
||
factPack: verifiedOfferPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
verifiedCandidate.whyItFits = '折扣10%,仅剩3件';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: verifiedCandidate,
|
||
strategy,
|
||
factPack: verifiedOfferPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/增强|不存在/u);
|
||
|
||
for (const unsupported of ['折扣10%,同时包邮', '折扣10%,另送赠品']) {
|
||
verifiedCandidate.whyItFits = unsupported;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: verifiedCandidate,
|
||
strategy,
|
||
factPack: verifiedOfferPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/增强|促销语义/u);
|
||
}
|
||
|
||
const freeShippingPack = makeFactPack({
|
||
verifiedOffer: { condition: '包邮' },
|
||
});
|
||
const freeShippingCandidate = makeCandidate(10);
|
||
freeShippingCandidate.whyItFits = '当前商品包邮';
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: freeShippingCandidate,
|
||
strategy,
|
||
factPack: freeShippingPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
freeShippingCandidate.whyItFits = '当前商品包邮并折扣10%';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: freeShippingCandidate,
|
||
strategy,
|
||
factPack: freeShippingPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/增强|促销语义/u);
|
||
|
||
for (const [offer, allowed, enhanced] of [
|
||
['满100减10', '满100减10', '满100减50'],
|
||
['立减10', '立减10', '立减50'],
|
||
] as const) {
|
||
const amountBoundPack = makeFactPack({ verifiedOffer: { condition: offer } });
|
||
const amountBoundCandidate = makeCandidate(10);
|
||
amountBoundCandidate.whyItFits = allowed;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: amountBoundCandidate,
|
||
strategy,
|
||
factPack: amountBoundPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
amountBoundCandidate.whyItFits = enhanced;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: amountBoundCandidate,
|
||
strategy,
|
||
factPack: amountBoundPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/不存在的数值/u);
|
||
}
|
||
|
||
for (const unsupportedPromotion of [
|
||
'半价', '五折', '第二件半价', '特价', '买二送一',
|
||
'half price', 'buy two get one free',
|
||
]) {
|
||
const unsupportedCandidate = makeCandidate(10);
|
||
unsupportedCandidate.whyItFits = unsupportedPromotion;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: unsupportedCandidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/未验证|促销/u);
|
||
}
|
||
|
||
const tenPercentPack = makeFactPack({ verifiedOffer: { condition: '10% off' } });
|
||
const halfPriceRewrite = makeCandidate(10);
|
||
halfPriceRewrite.whyItFits = '第二件半价';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: halfPriceRewrite,
|
||
strategy,
|
||
factPack: tenPercentPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/不存在的数值|增强/u);
|
||
|
||
const bundlePack = makeFactPack({ verifiedOffer: { condition: '买一送一' } });
|
||
const bundleCandidate = makeCandidate(10);
|
||
bundleCandidate.whyItFits = '买一送一';
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: bundleCandidate,
|
||
strategy,
|
||
factPack: bundlePack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
bundleCandidate.whyItFits = '买二送一';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: bundleCandidate,
|
||
strategy,
|
||
factPack: bundlePack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/不存在的数值/u);
|
||
|
||
for (const [offer, enhanced] of [
|
||
['第二件半价', '半价'],
|
||
['第二件5折', '5折'],
|
||
] as const) {
|
||
const contextualPack = makeFactPack({ verifiedOffer: { condition: offer } });
|
||
const contextualCandidate = makeCandidate(10);
|
||
contextualCandidate.whyItFits = offer;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: contextualCandidate,
|
||
strategy,
|
||
factPack: contextualPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
contextualCandidate.whyItFits = enhanced;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: contextualCandidate,
|
||
strategy,
|
||
factPack: contextualPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/不存在的数值/u);
|
||
}
|
||
});
|
||
|
||
it('策略 Provider 偶发扩写促销时本地确定性删除,原真实性门禁仍然生效', () => {
|
||
const promotionalStrategies = makeStrategies();
|
||
promotionalStrategies[0] = {
|
||
...promotionalStrategies[0]!,
|
||
firstFrame: '一个顾客拿起商品进行演示',
|
||
productBridge: '女性模特造型',
|
||
relevanceBeat: ids.image.split('').join(' '),
|
||
openingLine: 'Limited time discount — only 3 left.',
|
||
ctaStrategy: '限时折扣,仅剩3件,立即购买',
|
||
requiredMaterials: ['商品参考图', '优惠券截图'],
|
||
};
|
||
const factPack = makeFactPack();
|
||
const normalized = normalizeTkPromptStrategyPromotions({
|
||
strategySet: { strategies: promotionalStrategies },
|
||
factPack,
|
||
});
|
||
|
||
expect(normalized.strategies[0]).toMatchObject({
|
||
firstFrame: '首帧立即展示商品的可见结构或操作动作',
|
||
productBridge: '沿同一动作进入商品结构与操作细节',
|
||
relevanceBeat: '沿同一商品动作展示它与使用场景的关系',
|
||
openingLine: '',
|
||
ctaStrategy: '引导观众点击商品卡查看商品详情',
|
||
requiredMaterials: ['商品参考图'],
|
||
});
|
||
expect(validateTkPromptStrategySet({
|
||
strategySet: normalized,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toHaveLength(3);
|
||
|
||
const verifiedFactPack = makeFactPack({
|
||
verifiedOffer: { condition: '已验证折扣 10%' },
|
||
});
|
||
promotionalStrategies[0] = {
|
||
...promotionalStrategies[0]!,
|
||
openingLine: '已验证折扣 10%',
|
||
ctaStrategy: '查看已验证折扣 10%',
|
||
requiredMaterials: [],
|
||
};
|
||
const retained = normalizeTkPromptStrategyPromotions({
|
||
strategySet: { strategies: promotionalStrategies },
|
||
factPack: verifiedFactPack,
|
||
});
|
||
expect(retained.strategies[0]?.openingLine).toBe('已验证折扣 10%');
|
||
expect(retained.strategies[0]?.ctaStrategy).toBe('查看已验证折扣 10%');
|
||
});
|
||
|
||
it('商品事实百分比不被当作促销,但不能授权新的折扣数值', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
for (const factualNumber of ['过滤99%颗粒', '含50%成分']) {
|
||
const factPack = makeFactPack({
|
||
facts: [{
|
||
factId: 'visible-001',
|
||
status: 'visible',
|
||
text: factualNumber,
|
||
sourceImageIds: [ids.image],
|
||
}],
|
||
});
|
||
const candidate = makeCandidate(10);
|
||
candidate.coreInformationPoints[0]!.message = factualNumber;
|
||
candidate.beats[0]!.voiceoverTarget = factualNumber;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
}
|
||
|
||
const numericFact = '过滤99%颗粒';
|
||
const discountedPack = makeFactPack({
|
||
facts: [{
|
||
factId: 'visible-001',
|
||
status: 'visible',
|
||
text: numericFact,
|
||
sourceImageIds: [ids.image],
|
||
}],
|
||
verifiedOffer: { condition: '10% off' },
|
||
});
|
||
const valid = makeCandidate(10);
|
||
valid.coreInformationPoints[0]!.message = numericFact;
|
||
valid.beats[0]!.voiceoverTarget = `${numericFact}. 10% off.`;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: valid,
|
||
strategy,
|
||
factPack: discountedPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
|
||
const promotionRewrite = makeCandidate(10);
|
||
promotionRewrite.coreInformationPoints[0]!.message = numericFact;
|
||
promotionRewrite.beats[0]!.voiceoverTarget = '99% off.';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: promotionRewrite,
|
||
strategy,
|
||
factPack: discountedPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/不存在的数值/u);
|
||
});
|
||
|
||
it('单方案 Provider 偶发扩写促销时逐字段中和,原真实性门禁保持', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const factPack = makeFactPack();
|
||
const candidate = makeCandidate(10);
|
||
candidate.title = 'Limited time sale';
|
||
candidate.whyItFits = ids.image.split('').join(' ');
|
||
candidate.coreInformationPoints[0]!.message = '50% off';
|
||
candidate.beats[0]!.environment = 'Sale price display';
|
||
candidate.beats[0]!.editingCue = 'Add an on-screen text overlay';
|
||
candidate.beats[0]!.shotSize = '女性模特面部特写';
|
||
candidate.beats[0]!.lighting = '女性模特轮廓光';
|
||
candidate.beats[1]!.subject = '成年女性';
|
||
candidate.beats[1]!.action = '拿起商品进行演示';
|
||
candidate.beats[2]!.editingCue = '成年女性';
|
||
candidate.beats[3]!.environment = '拿起商品';
|
||
candidate.beats[3]!.subject = '女性模特造型';
|
||
candidate.beats[0]!.voiceoverTarget = 'Get 50% off today.';
|
||
candidate.beats[0]!.voiceoverZh = '今天五折优惠';
|
||
candidate.requiredMaterials = ['商品参考图', '优惠券截图'];
|
||
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/未验证|文字浮层|真人/u);
|
||
|
||
const normalized = normalizeTkPromptCandidateFreeText({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
});
|
||
expect(normalized.requiredMaterials).toEqual(['商品参考图']);
|
||
expect(normalized.coreInformationPoints[0]?.message)
|
||
.toBe('展示已确认商品事实:商品表面有可见网格结构');
|
||
expect(normalized.whyItFits).toBe('围绕已验证商品事实展示结构与操作方式');
|
||
expect(normalized.beats[0]?.editingCue).toBe('保持动作连续');
|
||
expect(normalized.beats[0]?.shotSize).toBe('商品中近景');
|
||
expect(normalized.beats[0]?.lighting).toBe('自然清晰光线');
|
||
expect(normalized.beats[1]).toMatchObject({
|
||
subject: '商品主体及其可见结构',
|
||
action: '连续展示商品结构与操作方式',
|
||
});
|
||
expect(normalized.beats[2]?.editingCue).toBe('保持动作连续');
|
||
expect(normalized.beats[3]?.environment).toBe('清晰、简洁的商品展示场景');
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: normalized,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
});
|
||
|
||
it('英文镜头叙述确定性正规化为中文,同时保留目标语言和合法品牌型号', () => {
|
||
const candidate = makeCandidate(10);
|
||
candidate.title = 'The ultimate product demonstration';
|
||
candidate.whyItFits = 'This strategy shows why the product is useful';
|
||
candidate.coreInformationPoints[0]!.message = 'Show the ACME X200 Logo clearly';
|
||
candidate.beats.forEach(beat => {
|
||
beat.environment = 'A clean modern kitchen counter';
|
||
beat.subject = 'The ACME X200 product and its visible Logo';
|
||
beat.action = 'Push the button and show the mechanism';
|
||
beat.shotSize = 'Extreme close up product shot';
|
||
beat.lighting = 'Soft cinematic studio lighting';
|
||
beat.editingCue = 'Cut quickly when the button moves';
|
||
beat.soundDesign = 'Natural click sound and room ambience';
|
||
beat.voiceoverTarget = 'Mira los detalles del ACME X200 en acción.';
|
||
beat.voiceoverZh = '查看 ACME X200 的商品细节与操作方式。';
|
||
});
|
||
candidate.requiredMaterials = ['ACME X200 product images'];
|
||
const factPack = makeFactPack({
|
||
productName: 'ACME X200',
|
||
targetMarket: '墨西哥',
|
||
spokenLanguage: '西班牙语',
|
||
facts: [{
|
||
factId: 'visible-001',
|
||
status: 'visible',
|
||
text: 'ACME X200 正面可见 Logo 和网格结构',
|
||
sourceImageIds: [ids.image],
|
||
}, {
|
||
factId: 'confirmed-002',
|
||
status: 'user-confirmed',
|
||
text: '用户确认已提供未拆封包装素材',
|
||
sourceImageIds: [],
|
||
}],
|
||
});
|
||
const normalized = normalizeTkPromptCandidateFreeText({
|
||
candidate,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '西班牙语',
|
||
});
|
||
|
||
const nonVoiceoverFields = [
|
||
normalized.title,
|
||
normalized.whyItFits,
|
||
...normalized.coreInformationPoints.map(point => point.message),
|
||
...normalized.beats.flatMap(beat => [
|
||
beat.environment,
|
||
beat.subject,
|
||
beat.action,
|
||
beat.shotSize,
|
||
beat.lighting,
|
||
beat.editingCue,
|
||
beat.soundDesign,
|
||
beat.voiceoverZh,
|
||
]),
|
||
...normalized.requiredMaterials,
|
||
].filter(Boolean);
|
||
expect(nonVoiceoverFields.every(value => (
|
||
isTkPromptChineseNarrative(value, ['ACME X200'])
|
||
))).toBe(true);
|
||
expect(normalized.beats.every(beat => beat.voiceoverTarget.includes('Mira los detalles')))
|
||
.toBe(true);
|
||
expect(JSON.stringify(normalized)).toContain('ACME X200');
|
||
});
|
||
|
||
it('服务端确定性锁定时长、事实、时间轴、人物、图片与口播结构', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const factPack = makeFactPack();
|
||
const candidate = makeCandidate(10);
|
||
candidate.coreInformationPoints = [{
|
||
messageId: 'message-003',
|
||
message: '包含越权事实的描述',
|
||
factIds: ['visible-999', 'visible-001', 'visible-001'],
|
||
}, {
|
||
messageId: 'message-003',
|
||
message: '只有越权事实的描述',
|
||
factIds: ['visible-999'],
|
||
}, {
|
||
messageId: 'message-002',
|
||
message: '超出 10 秒上限的第三点',
|
||
factIds: ['visible-001'],
|
||
}];
|
||
candidate.primaryActionId = 'action-999';
|
||
candidate.beats.forEach((beat, index) => {
|
||
beat.startSeconds = index + 0.25;
|
||
beat.endSeconds = index + 0.75;
|
||
beat.actionPhase = 'start';
|
||
beat.productImageIndex = 9;
|
||
beat.presenterVisible = true;
|
||
beat.voiceoverTarget = '';
|
||
beat.voiceoverZh = '';
|
||
});
|
||
|
||
const structured = normalizeTkPromptCandidateStructure({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageCount: 1,
|
||
});
|
||
expect(structured.coreInformationPoints).toEqual([{
|
||
messageId: 'message-001',
|
||
message: '商品表面有可见网格结构',
|
||
factIds: ['visible-001'],
|
||
}, {
|
||
messageId: 'message-002',
|
||
message: '商品表面有可见网格结构',
|
||
factIds: ['visible-001'],
|
||
}]);
|
||
expect(structured.primaryActionId).toBeNull();
|
||
expect(structured.beats.map(beat => [
|
||
beat.startSeconds,
|
||
beat.endSeconds,
|
||
beat.actionPhase,
|
||
beat.productImageIndex,
|
||
beat.presenterVisible,
|
||
])).toEqual([
|
||
[0, 1, null, 1, false],
|
||
[1, 3, null, 1, false],
|
||
[3, 7, null, 1, false],
|
||
[7, 10, null, 1, false],
|
||
]);
|
||
expect(structured.beats.every(beat => beat.voiceoverTarget.length > 0)).toBe(true);
|
||
expect(structured.beats.every(beat => /[\u3400-\u9fff]/u.test(beat.voiceoverZh))).toBe(true);
|
||
|
||
const closed = normalizeTkPromptCandidateBeatInformation({
|
||
candidate: structured,
|
||
durationSeconds: 10,
|
||
});
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: closed,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
|
||
const five = makeCandidate(5);
|
||
five.primaryActionId = 'action-999';
|
||
five.beats.forEach(beat => {
|
||
beat.actionPhase = null;
|
||
beat.voiceoverTarget = 'Forbidden voice';
|
||
beat.voiceoverZh = '禁止口播';
|
||
});
|
||
const fiveStructured = normalizeTkPromptCandidateStructure({
|
||
candidate: five,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageCount: 1,
|
||
});
|
||
expect(fiveStructured.primaryActionId).toBe('action-001');
|
||
expect(fiveStructured.beats.map(beat => beat.actionPhase))
|
||
.toEqual(['start', 'progress', 'complete']);
|
||
expect(fiveStructured.beats.every(beat => (
|
||
beat.voiceoverTarget === '' && beat.voiceoverZh === ''
|
||
))).toBe(true);
|
||
});
|
||
|
||
it('forbiddenClaims 会剥离中英文禁止前缀后匹配核心禁词', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
for (const [forbiddenClaim, leakedText] of [
|
||
['不要提医疗功效', '医疗功效'],
|
||
['禁止出现治愈焦虑', '治愈焦虑'],
|
||
['不得声称永久有效', '永久有效'],
|
||
['avoid medical claims', 'medical claims'],
|
||
['never say cures anxiety', 'cures anxiety'],
|
||
['do not mention permanent results', 'permanent results'],
|
||
] as const) {
|
||
const candidate = makeCandidate(10);
|
||
candidate.beats[0]!.voiceoverTarget = `Product detail: ${leakedText}`;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ forbiddenClaims: [forbiddenClaim] }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/禁止声明/u);
|
||
}
|
||
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: makeCandidate(10),
|
||
strategy,
|
||
factPack: makeFactPack({ forbiddenClaims: ['不要提医疗功效'] }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toBeTruthy();
|
||
});
|
||
|
||
it('5 秒执行 Prompt 没有口播、中文译文和全局语言硬约束', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(5);
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image, ids.image2],
|
||
});
|
||
expect(prompt).not.toContain('旁白(');
|
||
expect(prompt).not.toContain('仅供审核');
|
||
expect(prompt).toContain('@图片1=上传顺序第1张');
|
||
expect(prompt).toContain('@图片2=上传顺序第2张');
|
||
expect(prompt).not.toContain(ids.image);
|
||
expect(prompt).not.toContain(ids.image2);
|
||
expect(prompt).not.toMatch(/AUDIO LANGUAGE LOCK|音频语言硬约束/iu);
|
||
expect(prompt.length).toBeLessThanOrEqual(3_800);
|
||
});
|
||
|
||
it('5 秒 live_presenter 只生成同一虚构主持人的无声动作,不附演员素材', () => {
|
||
const strategy: TkPromptStrategy = {
|
||
...makeStrategies()[0]!,
|
||
videoMode: 'live_presenter',
|
||
};
|
||
const candidate = makeCandidate(5);
|
||
candidate.beats[0]!.presenterVisible = true;
|
||
const factPack = makeFactPack({ targetMarket: 'Japan', spokenLanguage: null });
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
expect(prompt).toContain(TK_PROMPT_LIVE_PRESENTER_BOUNDARY);
|
||
expect(prompt).toContain('主持人仅作无声动作演示');
|
||
expect(prompt).not.toContain('旁白(');
|
||
expect(prompt).not.toMatch(/authorizedActorAssetId|已绑定授权演员|@演员/u);
|
||
});
|
||
|
||
it('最终 renderer 拒绝事实包注入的素材 ID、禁用声明和未验证促销', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const fiveCandidate = makeCandidate(5);
|
||
const renderFiveSeconds = (factPack: TkPromptFactPack) => renderTkPromptExecutionPrompt({
|
||
candidate: fiveCandidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
productName: `参考商品 ${ids.image}`,
|
||
}))).toThrow(/素材 UUID|内部标识/u);
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction(`手部按下结构 ${ids.image}`)],
|
||
}))).toThrow(/素材 UUID|内部标识/u);
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction('手部按下 action-001 对应结构')],
|
||
}))).toThrow(/内部标识/u);
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
productName: '治愈焦虑参考商品',
|
||
forbiddenClaims: ['治愈焦虑'],
|
||
}))).toThrow(/禁止声明/u);
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction('手部按下结构并宣称治愈焦虑')],
|
||
forbiddenClaims: ['治愈焦虑'],
|
||
}))).toThrow(/禁止声明/u);
|
||
for (const productName of [
|
||
'拥有10年经验与十亿 GMV 的专家推荐商品',
|
||
TK_PROMPT_HOOK_PATTERNS[0]!.internalExample,
|
||
'参考 @图片1 商品',
|
||
]) {
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
productName,
|
||
}))).toThrow(/履历|专家|Hook|图片引用/u);
|
||
}
|
||
for (const action of [
|
||
'专家用10年经验和 GMV 背书并按下结构',
|
||
TK_PROMPT_HOOK_PATTERNS[1]!.internalExample,
|
||
'手部按下 @图片1 中的结构',
|
||
]) {
|
||
expect(() => renderFiveSeconds(makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction(action)],
|
||
}))).toThrow(/履历|专家|Hook|图片引用/u);
|
||
}
|
||
|
||
expect(() => renderTkPromptExecutionPrompt({
|
||
candidate: makeCandidate(10),
|
||
strategy: { ...strategy, productBridge: '继续 @图片1 中的操作' },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/图片引用/u);
|
||
|
||
const tenCandidate = makeCandidate(10);
|
||
expect(() => renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: makeFactPack({ productName: '限时折扣10%参考商品' }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/未验证/u);
|
||
|
||
const verifiedOfferPack = makeFactPack({
|
||
productName: '折扣10%参考商品',
|
||
verifiedOffer: { condition: '折扣10%' },
|
||
});
|
||
expect(renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: verifiedOfferPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toContain('【商品】折扣10%参考商品');
|
||
|
||
for (const productName of ['折扣10%并包邮参考商品', '折扣10%并送赠品参考商品']) {
|
||
expect(() => renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: makeFactPack({
|
||
productName,
|
||
verifiedOffer: { condition: '折扣10%' },
|
||
}),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/增强|促销语义/u);
|
||
}
|
||
expect(() => renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: makeFactPack({
|
||
productName: '包邮并折扣10%参考商品',
|
||
verifiedOffer: { condition: '包邮' },
|
||
}),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/增强|促销语义/u);
|
||
expect(renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: makeFactPack({ productName: '含50%成分参考商品' }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toContain('【商品】含50%成分参考商品');
|
||
expect(renderTkPromptExecutionPrompt({
|
||
candidate: tenCandidate,
|
||
strategy,
|
||
factPack: makeFactPack({ productName: '标注保质期10年的参考商品' }),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toContain('【商品】标注保质期10年的参考商品');
|
||
});
|
||
|
||
it('5 秒只能选择并编译审核后仍有事实支撑的稳定 actionId', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(5);
|
||
const factPack = makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction('手部旋转可见结构', 'action-002')],
|
||
});
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toThrow(/主商品动作/u);
|
||
expect(() => compileTkPromptCandidateBeats({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
})).toThrow(/失效|事实支撑/u);
|
||
|
||
candidate.primaryActionId = 'action-002';
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
});
|
||
expect(renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
})).toContain('主商品动作:手部旋转可见结构');
|
||
});
|
||
|
||
it('5 秒非人物模式不把事实包中的模特动作重新注入最终 Prompt', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(5);
|
||
const factPack = makeFactPack({
|
||
spokenLanguage: null,
|
||
demonstrableActions: [makeDemonstrableAction('女性模特穿上商品并展示')],
|
||
});
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
|
||
expect(prompt).toContain('主商品动作:手部指向并展示图片中可见的商品特征');
|
||
expect(prompt).not.toMatch(/女性|模特/u);
|
||
});
|
||
|
||
it('5 秒允许否定式无人声说明,但拒绝正向人声;否定字幕规则不误伤', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(5);
|
||
candidate.beats[0]!.soundDesign = '真实卡扣声,无人声、无旁白';
|
||
candidate.warnings = ['禁止生成字幕'];
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toBeTruthy();
|
||
|
||
candidate.beats[0]!.soundDesign = 'Product click only; do not add voiceover.';
|
||
candidate.warnings = ['Do not show subtitles.'];
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toBeTruthy();
|
||
|
||
for (const safeSilence of [
|
||
'卡扣声;无解说、无配音、无语音',
|
||
'Product click only; no narration or vocals.',
|
||
'仅拟音,禁止加入旁白或对白',
|
||
]) {
|
||
candidate.beats[0]!.soundDesign = safeSilence;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toBeTruthy();
|
||
}
|
||
|
||
for (const humanVoice of [
|
||
'加入人声旁白和真实卡扣声',
|
||
'加入解说',
|
||
'使用配音',
|
||
'主持人讲话',
|
||
'add narration',
|
||
'include vocals',
|
||
'use spoken words',
|
||
]) {
|
||
candidate.beats[0]!.soundDesign = humanVoice;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toThrow(/人声/u);
|
||
}
|
||
|
||
candidate.beats[0]!.soundDesign = '真实卡扣声,无人声';
|
||
for (const overlay of [
|
||
'显示字幕并指向商品结构',
|
||
'屏幕显示一句文字',
|
||
'展示文案',
|
||
'出现贴纸',
|
||
'添加标签',
|
||
'show screen text',
|
||
'add a sticker',
|
||
]) {
|
||
candidate.beats[0]!.action = overlay;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toThrow(/字幕|文字浮层/u);
|
||
}
|
||
candidate.beats[0]!.action = '展示商品包装原有文字和 Logo';
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toBeTruthy();
|
||
|
||
const crossField = makeCandidate(5);
|
||
crossField.title = 'Show';
|
||
crossField.whyItFits = 'Title-led product detail';
|
||
expect(JSON.stringify(crossField)).toMatch(/show.{0,32}title/iu);
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: crossField,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
})).toBeTruthy();
|
||
});
|
||
|
||
it('10 秒保留目标口播,但中文审核译文绝不进入执行 Prompt', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(10);
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
});
|
||
expect(prompt).toContain('旁白(English):See this detail.');
|
||
expect(prompt).not.toContain('看清这个细节。');
|
||
expect(prompt).not.toMatch(/AUDIO LANGUAGE LOCK|音频语言硬约束/iu);
|
||
});
|
||
|
||
it.each(TK_PROMPT_LANGUAGES.flatMap(language => ([10, 15] as const).flatMap(duration => (
|
||
([1, 2, 3] as const).map(imageCount => ({ language, duration, imageCount }))
|
||
))))('$language / $duration 秒 / $imageCount 图执行 Prompt 只保留目标语言口播', ({ language, duration, imageCount }) => {
|
||
const sample = LANGUAGE_SAMPLES[language];
|
||
const candidate = makeCandidate(duration);
|
||
candidate.beats.forEach((beat, index) => {
|
||
beat.voiceoverTarget = `${sample.voiceover} ${index + 1}`;
|
||
beat.voiceoverZh = language === '普通话' ? '' : `审核译文${index + 1}`;
|
||
});
|
||
const factPack = makeFactPack({
|
||
targetMarket: '墨西哥',
|
||
spokenLanguage: language,
|
||
});
|
||
const strategy = makeStrategies()[0]!;
|
||
const imageAssetIds = [ids.image, ids.image2, ids.image3].slice(0, imageCount);
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: duration,
|
||
spokenLanguage: language,
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: duration,
|
||
spokenLanguage: language,
|
||
imageAssetIds,
|
||
});
|
||
expect(prompt).toContain(`旁白(${sample.label}):${sample.voiceover} 1`);
|
||
for (const beat of candidate.beats) {
|
||
expect(prompt).toContain(beat.voiceoverTarget);
|
||
if (beat.voiceoverZh) expect(prompt).not.toContain(beat.voiceoverZh);
|
||
}
|
||
for (let index = 1; index <= imageCount; index += 1) {
|
||
expect(prompt).toContain(`@图片${index}=上传顺序第${index}张`);
|
||
}
|
||
const nonVoiceoverPrompt = prompt
|
||
.split('\n')
|
||
.filter(line => !line.startsWith('旁白('))
|
||
.join('\n');
|
||
expect(isTkPromptChineseNarrative(nonVoiceoverPrompt)).toBe(true);
|
||
expect(prompt).not.toMatch(/AUDIO LANGUAGE LOCK|音频语言硬约束/iu);
|
||
expect(prompt.length).toBeLessThanOrEqual(3_800);
|
||
});
|
||
|
||
it.each(TK_PROMPT_LANGUAGES)('%s 的 live_presenter 使用目标市场虚构非名人并保持同一人物', language => {
|
||
const sample = LANGUAGE_SAMPLES[language];
|
||
const candidate = makeCandidate(10);
|
||
candidate.beats[0]!.presenterVisible = true;
|
||
candidate.beats.forEach((beat, index) => {
|
||
beat.voiceoverTarget = `${sample.voiceover} ${index + 1}`;
|
||
beat.voiceoverZh = language === '普通话' ? '' : `审核译文${index + 1}`;
|
||
});
|
||
const targetMarket = `${language}目标市场`;
|
||
const factPack = makeFactPack({ targetMarket, spokenLanguage: language });
|
||
const strategy: TkPromptStrategy = {
|
||
...makeStrategies()[0]!,
|
||
videoMode: 'live_presenter',
|
||
};
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: language,
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: language,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
expect(prompt).toContain(`【目标市场】${targetMarket}`);
|
||
expect(prompt).toContain(TK_PROMPT_LIVE_PRESENTER_BOUNDARY);
|
||
expect(prompt).toContain(`口型与各镜头的${language}旁白自然对应`);
|
||
expect(prompt).toContain(`旁白(${sample.label})`);
|
||
expect(prompt).not.toMatch(/authorizedActorAssetId|已绑定授权演员|@演员/u);
|
||
});
|
||
|
||
it('策略、分镜和执行 Prompt 均拒绝虚构主持人的使用经历或测评身份', () => {
|
||
const strategies = makeStrategies();
|
||
strategies[0] = {
|
||
...strategies[0]!,
|
||
videoMode: 'live_presenter',
|
||
productBridge: '我亲自用了三个月后推荐给大家',
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/个人使用经历|测评身份/u);
|
||
|
||
const candidate = makeCandidate(10);
|
||
candidate.beats[0]!.presenterVisible = true;
|
||
candidate.beats[0]!.voiceoverTarget = 'I personally tested this product for weeks.';
|
||
candidate.beats[0]!.voiceoverZh = '我亲自测试了这个商品数周。';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy: { ...makeStrategies()[0]!, videoMode: 'live_presenter' },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow(/个人使用经历|测评身份/u);
|
||
});
|
||
|
||
it('10/15 秒核心信息点上限、顺序和事实闭包是硬门禁', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const tenSeconds = makeCandidate(10);
|
||
tenSeconds.coreInformationPoints.push({
|
||
messageId: 'message-002',
|
||
message: '同一结构可通过操作直接展示',
|
||
factIds: ['visible-001'],
|
||
});
|
||
tenSeconds.beats[1]!.coreInformationPointIds = ['message-002'];
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: tenSeconds,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
}).coreInformationPoints).toHaveLength(2);
|
||
|
||
tenSeconds.coreInformationPoints.push({
|
||
messageId: 'message-003',
|
||
message: '第三个卖点',
|
||
factIds: ['visible-001'],
|
||
});
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: tenSeconds,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/核心信息点|上限/u);
|
||
|
||
const fifteenSeconds = makeCandidate(15);
|
||
fifteenSeconds.coreInformationPoints.push(
|
||
{ messageId: 'message-002', message: '第二个信息点', factIds: ['visible-001'] },
|
||
{ messageId: 'message-003', message: '第三个信息点', factIds: ['visible-001'] },
|
||
);
|
||
fifteenSeconds.beats[1]!.coreInformationPointIds = ['message-002'];
|
||
fifteenSeconds.beats[2]!.coreInformationPointIds = ['message-003'];
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: fifteenSeconds,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
}).coreInformationPoints).toHaveLength(3);
|
||
|
||
fifteenSeconds.beats[0]!.factIds = [];
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: fifteenSeconds,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/闭合/u);
|
||
});
|
||
|
||
it('5 秒唯一主动作和三阶段由编译器注入公共 beats', () => {
|
||
const candidate = makeCandidate(5);
|
||
const strategy = makeStrategies()[0]!;
|
||
const factPack = makeFactPack({ spokenLanguage: null });
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
const beats = compileTkPromptCandidateBeats({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
});
|
||
expect(beats.map(beat => beat.action)).toEqual([
|
||
expect.stringContaining('主商品动作:手部指向并按下可见结构;阶段:启动'),
|
||
expect.stringContaining('阶段:连续推进'),
|
||
expect.stringContaining('阶段:完成'),
|
||
]);
|
||
expect(beats[0]!.subject).toContain('@图片1中的同一主商品');
|
||
expect(beats[0]).not.toHaveProperty('primaryActionId');
|
||
|
||
candidate.beats[1]!.actionPhase = 'complete';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/actionPhase|阶段/u);
|
||
});
|
||
|
||
it('live_presenter 至少一拍使用同一虚构主持人,内部布尔值不泄漏到公共 beat', () => {
|
||
const strategy: TkPromptStrategy = {
|
||
...makeStrategies()[0]!,
|
||
videoMode: 'live_presenter',
|
||
};
|
||
const candidate = makeCandidate(10);
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/live_presenter|主持人/u);
|
||
candidate.beats[0]!.presenterVisible = true;
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
const beats = compileTkPromptCandidateBeats({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
});
|
||
expect(beats[0]!.subject).toContain('同一位成年虚构非名人主持人');
|
||
expect(beats[0]).not.toHaveProperty('presenterVisible');
|
||
});
|
||
|
||
it('参考图整数索引和运镜连续段预算不可由自由文本绕过', () => {
|
||
const strategy = makeStrategies()[0]!;
|
||
const candidate = makeCandidate(10);
|
||
candidate.beats[0]!.productImageIndex = 2;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/参考图/u);
|
||
|
||
candidate.beats[0]!.productImageIndex = 1;
|
||
candidate.beats[0]!.subject = '自由伪造 @图片1 引用';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/自由文本|素材编号/u);
|
||
|
||
for (const personInstruction of [
|
||
'随机女孩出镜并拿起商品',
|
||
'一个顾客出镜拿起商品',
|
||
'店员出现并演示商品',
|
||
'妈妈露脸口播',
|
||
'学生手持商品出镜',
|
||
'a customer appears and holds the product',
|
||
'feature a shopper who demonstrates the product',
|
||
'a creator speaks to camera',
|
||
]) {
|
||
const unauthorizedPerson = makeCandidate(10);
|
||
unauthorizedPerson.beats[0]!.subject = personInstruction;
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: unauthorizedPerson,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/真人|人物/u);
|
||
}
|
||
|
||
for (const facelessInstruction of [
|
||
'仅用户手部操作商品',
|
||
'无脸手部特写',
|
||
'第一视角 POV 操作商品',
|
||
'faceless POV product operation',
|
||
]) {
|
||
const faceless = makeCandidate(10);
|
||
faceless.beats[0]!.subject = facelessInstruction;
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: faceless,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
}
|
||
|
||
const dense = makeCandidate(10);
|
||
dense.beats.map((beat, index) => {
|
||
beat.movementId = (['locked_off', 'slow_push_in', 'locked_off', 'locked_off'] as const)[index]!;
|
||
return beat;
|
||
});
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: dense,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/运镜|连续段/u);
|
||
const normalizedDense = normalizeTkPromptCandidateCameraMovements({
|
||
candidate: dense,
|
||
durationSeconds: 10,
|
||
});
|
||
expect(normalizedDense.beats.map(beat => beat.movementId)).toEqual([
|
||
'locked_off',
|
||
'slow_push_in',
|
||
'slow_push_in',
|
||
'slow_push_in',
|
||
]);
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: normalizedDense,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
|
||
const brokenInformationReference = makeCandidate(10);
|
||
brokenInformationReference.beats[0]!.coreInformationPointIds = [];
|
||
brokenInformationReference.beats[0]!.factIds = [];
|
||
brokenInformationReference.beats[3]!.coreInformationPointIds = ['message-001'];
|
||
brokenInformationReference.beats[3]!.factIds = ['visible-001'];
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: brokenInformationReference,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/核心信息点|引用数量/u);
|
||
const normalizedInformation = normalizeTkPromptCandidateBeatInformation({
|
||
candidate: brokenInformationReference,
|
||
durationSeconds: 10,
|
||
});
|
||
expect(normalizedInformation.beats.map(beat => ({
|
||
points: beat.coreInformationPointIds,
|
||
facts: beat.factIds,
|
||
}))).toEqual([
|
||
{ points: ['message-001'], facts: ['visible-001'] },
|
||
{ points: ['message-001'], facts: ['visible-001'] },
|
||
{ points: ['message-001'], facts: ['visible-001'] },
|
||
{ points: [], facts: [] },
|
||
]);
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: normalizedInformation,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
|
||
const fiveDense = makeCandidate(5);
|
||
fiveDense.beats[1]!.movementId = 'locked_off';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: fiveDense,
|
||
strategy,
|
||
factPack: makeFactPack({ spokenLanguage: null }),
|
||
durationSeconds: 5,
|
||
spokenLanguage: null,
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/运镜|连续段/u);
|
||
|
||
const fifteenAtBudget = makeCandidate(15);
|
||
(['locked_off', 'locked_off', 'slow_push_in', 'slow_push_in', 'macro_follow'] as const)
|
||
.forEach((movementId, index) => { fifteenAtBudget.beats[index]!.movementId = movementId; });
|
||
expect(validateTkPromptOptionCandidate({
|
||
candidate: fifteenAtBudget,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toBeTruthy();
|
||
fifteenAtBudget.beats[3]!.movementId = 'lateral_track';
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate: fifteenAtBudget,
|
||
strategy,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
})).toThrow(/运镜|连续段/u);
|
||
});
|
||
|
||
it('最终 Prompt 门禁拒绝越界引用和中英文正向文字层指令', () => {
|
||
const valid = [
|
||
'【参考图固定映射】@图片1=上传顺序第1张',
|
||
'0–1秒:@图片1中的同一主商品。',
|
||
'1–3秒:@图片1中的同一主商品。',
|
||
'3–7秒:@图片1中的同一主商品。',
|
||
'7–10秒:@图片1中的同一主商品。',
|
||
TK_PROMPT_NON_LIVE_PERSON_BOUNDARY,
|
||
TK_PROMPT_VISUAL_TEXT_BOUNDARY,
|
||
].join('\n');
|
||
expect(validateTkPromptFinalExecutionPrompt({
|
||
prompt: valid,
|
||
imageCount: 1,
|
||
videoMode: 'product_closeup',
|
||
durationSeconds: 10,
|
||
})).toBe(valid);
|
||
for (const violation of [
|
||
'@图片2中的商品',
|
||
'@图片1.5中的商品',
|
||
'@视频1',
|
||
'show a $9 price sticker',
|
||
'add a speech bubble',
|
||
'显示字幕',
|
||
'屏幕显示一句文字',
|
||
'展示文案',
|
||
'出现贴纸',
|
||
'添加标签',
|
||
'随机女孩出镜并演示商品',
|
||
'一个顾客出镜拿起商品',
|
||
'a shopper appears and holds the product',
|
||
'AUDIO LANGUAGE LOCK: English only',
|
||
'generated by AI',
|
||
'#AIGC',
|
||
'AI创作',
|
||
'人工智能制作',
|
||
'拥有10年经验和十亿 GMV 的专家推荐',
|
||
TK_PROMPT_HOOK_PATTERNS[0]!.internalExample,
|
||
'@图片1中的额外自由引用',
|
||
]) {
|
||
expect(() => validateTkPromptFinalExecutionPrompt({
|
||
prompt: `${valid}\n${violation}`,
|
||
imageCount: 1,
|
||
videoMode: 'product_closeup',
|
||
durationSeconds: 10,
|
||
})).toThrow();
|
||
}
|
||
for (const safeSuffix of [
|
||
'展示商品包装原有文字和 Logo',
|
||
'仅手部操作与第一视角 POV',
|
||
'展示商品型号 AIGCLEAN-200',
|
||
]) {
|
||
expect(validateTkPromptFinalExecutionPrompt({
|
||
prompt: `${valid}\n${safeSuffix}`,
|
||
imageCount: 1,
|
||
videoMode: 'product_closeup',
|
||
durationSeconds: 10,
|
||
})).toContain(safeSuffix);
|
||
}
|
||
const soundOnlyValid = [
|
||
'【参考图固定映射】@图片1=上传顺序第1张',
|
||
'0–1秒:@图片1中的商品。',
|
||
'1–3秒:@图片1中的商品。',
|
||
'3–5秒:@图片1中的商品。',
|
||
TK_PROMPT_NON_LIVE_PERSON_BOUNDARY,
|
||
TK_PROMPT_VISUAL_TEXT_BOUNDARY,
|
||
].join('\n');
|
||
expect(validateTkPromptFinalExecutionPrompt({
|
||
prompt: `${soundOnlyValid}\n无解说、无配音、无人声`,
|
||
imageCount: 1,
|
||
videoMode: 'product_closeup',
|
||
durationSeconds: 5,
|
||
})).toContain('无解说、无配音、无人声');
|
||
for (const soundOnlyViolation of [
|
||
'加入人声旁白', '加入解说', '使用配音',
|
||
'add narration', 'include vocals', 'spoken words', '限时优惠10%',
|
||
]) {
|
||
expect(() => validateTkPromptFinalExecutionPrompt({
|
||
prompt: `${soundOnlyValid}\n${soundOnlyViolation}`,
|
||
imageCount: 1,
|
||
videoMode: 'product_closeup',
|
||
durationSeconds: 5,
|
||
})).toThrow();
|
||
}
|
||
});
|
||
|
||
it('非真人模式的人物门禁只检查视觉指令,不误伤音频中的声音特征', () => {
|
||
const candidate = makeCandidate(10);
|
||
candidate.beats[0]!.soundDesign = '女性旁白自然口播,配合轻微环境声';
|
||
const strategy = makeStrategies()[0]!;
|
||
const factPack = makeFactPack();
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
expect(prompt).toContain('声音:女性旁白自然口播');
|
||
|
||
expect(() => validateTkPromptFinalExecutionPrompt({
|
||
prompt: prompt.replace('画面:', '画面:女性模特出镜展示商品;'),
|
||
imageCount: 1,
|
||
videoMode: 'scene_demo',
|
||
durationSeconds: 10,
|
||
})).toThrow(/Prompt 不得生成真人/u);
|
||
});
|
||
|
||
it('新方案不再接受发布文案产物', () => {
|
||
const candidate = { ...makeCandidate(10), publishPack: { title: '旧发布标题' } };
|
||
expect(() => validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy: makeStrategies()[0]!,
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
spokenLanguage: '英语',
|
||
})).toThrow();
|
||
});
|
||
|
||
it('15 秒长字段只在总长超限时由编译器渐进压缩并保留全部硬边界', () => {
|
||
const candidate = makeCandidate(15);
|
||
candidate.title = '商品方案'.repeat(20);
|
||
candidate.beats.forEach(beat => {
|
||
beat.environment = '真实连续场景'.repeat(70);
|
||
beat.subject = '同一商品主体'.repeat(55);
|
||
beat.action = '连续展示商品动作'.repeat(45);
|
||
beat.shotSize = '商品近景'.repeat(25);
|
||
beat.lighting = '自然清晰光线'.repeat(40);
|
||
beat.editingCue = '保持动作连续'.repeat(40);
|
||
beat.soundDesign = '自然环境声与商品操作声'.repeat(35);
|
||
beat.voiceoverTarget = 'See the product detail in action. '.repeat(20);
|
||
beat.voiceoverZh = '查看商品细节与操作方式。'.repeat(30);
|
||
});
|
||
const strategy = makeStrategies()[0]!;
|
||
const factPack = makeFactPack();
|
||
const validated = validateTkPromptOptionCandidate({
|
||
candidate,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
const prompt = renderTkPromptExecutionPrompt({
|
||
candidate: validated,
|
||
strategy,
|
||
factPack,
|
||
durationSeconds: 15,
|
||
spokenLanguage: '英语',
|
||
imageAssetIds: [ids.image],
|
||
});
|
||
expect(prompt.length).toBeLessThanOrEqual(3_800);
|
||
expect(prompt.match(/@图片1/gu)).toHaveLength(6);
|
||
expect(prompt).toContain(TK_PROMPT_NON_LIVE_PERSON_BOUNDARY);
|
||
expect(prompt).toContain(TK_PROMPT_VISUAL_TEXT_BOUNDARY);
|
||
});
|
||
|
||
it('Hook 证据推导逐条感知否定且不把方位“前后”当前后对比', () => {
|
||
expect(deriveTkPromptEvidenceFlags({
|
||
userConfirmedFacts: [
|
||
'没有竞品对比素材',
|
||
'无真实评论原文',
|
||
'禁止虚构真实经历',
|
||
'产品前后有两个按钮',
|
||
'无10%折扣证据',
|
||
],
|
||
verifiedOffer: '',
|
||
})).toEqual(evidenceFlags());
|
||
});
|
||
|
||
it('三案任意一对只有三维不同时硬拒绝', () => {
|
||
const strategies = makeStrategies();
|
||
strategies[1] = {
|
||
...strategies[1]!,
|
||
styleForm: strategies[0]!.styleForm,
|
||
style: strategies[0]!.style,
|
||
hookPatternId: strategies[0]!.hookPatternId,
|
||
};
|
||
expect(() => validateTkPromptStrategySet({
|
||
strategySet: { strategies },
|
||
factPack: makeFactPack(),
|
||
durationSeconds: 10,
|
||
evidenceFlags: { ...evidenceFlags(), sealedPackage: true },
|
||
})).toThrow(/六维|差异/u);
|
||
});
|
||
});
|
||
|
||
const LANGUAGE_SAMPLES = {
|
||
'英语': { label: 'English', voiceover: 'See this detail.' },
|
||
'西班牙语': { label: 'Spanish', voiceover: 'Mira este detalle.' },
|
||
'葡萄牙语': { label: 'Portuguese', voiceover: 'Veja este detalhe.' },
|
||
'法语': { label: 'French', voiceover: 'Regardez ce détail.' },
|
||
'印尼语': { label: 'Indonesian', voiceover: 'Lihat detail ini.' },
|
||
'意大利语': { label: 'Italian', voiceover: 'Guarda questo dettaglio.' },
|
||
'日语': { label: 'Japanese', voiceover: 'この細部を見てください。' },
|
||
'马来语': { label: 'Malay', voiceover: 'Lihat butiran ini.' },
|
||
'泰语': { label: 'Thai', voiceover: 'ดูรายละเอียดนี้' },
|
||
'越南语': { label: 'Vietnamese', voiceover: 'Hãy xem chi tiết này.' },
|
||
'普通话': { label: 'Mandarin Chinese', voiceover: '看看这个细节。' },
|
||
'德语': { label: 'German', voiceover: 'Sieh dir dieses Detail an.' },
|
||
'荷兰语': { label: 'Dutch', voiceover: 'Bekijk dit detail.' },
|
||
'波兰语': { label: 'Polish', voiceover: 'Spójrz na ten szczegół.' },
|
||
} as const;
|
||
|
||
function baseRequest() {
|
||
return {
|
||
imageAssetIds: [ids.image],
|
||
durationSeconds: 10,
|
||
targetMarket: 'United States',
|
||
spokenLanguage: '英语',
|
||
userConfirmedFacts: [],
|
||
forbiddenClaims: [],
|
||
evidenceFlags: evidenceFlags(),
|
||
verifiedOffer: null,
|
||
};
|
||
}
|
||
|
||
function evidenceFlags() {
|
||
return {
|
||
beforeAfter: false,
|
||
competitor: false,
|
||
numeric: false,
|
||
realComment: false,
|
||
popularity: false,
|
||
honestExperience: false,
|
||
sealedPackage: false,
|
||
};
|
||
}
|
||
|
||
function makeFactPack(
|
||
overrides: Partial<TkPromptFactPack> = {},
|
||
): TkPromptFactPack {
|
||
return {
|
||
productName: '参考商品',
|
||
category: '家居用品',
|
||
confidence: 'high',
|
||
facts: [{
|
||
factId: 'visible-001',
|
||
status: 'visible',
|
||
text: '商品表面有可见网格结构',
|
||
sourceImageIds: [ids.image],
|
||
}, {
|
||
factId: 'confirmed-002',
|
||
status: 'user-confirmed',
|
||
text: '用户确认已提供未拆封包装素材',
|
||
sourceImageIds: [],
|
||
}],
|
||
internalHypotheses: [],
|
||
demonstrableActions: [makeDemonstrableAction('手部指向并按下可见结构')],
|
||
forbiddenClaims: [],
|
||
availableMaterials: ['product-images'],
|
||
targetMarket: 'United States',
|
||
spokenLanguage: '英语',
|
||
verifiedOffer: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function makeDemonstrableAction(text: string, actionId = 'action-001') {
|
||
return {
|
||
actionId,
|
||
text,
|
||
supportingFactIds: ['visible-001'],
|
||
sourceImageAssetIds: [ids.image],
|
||
};
|
||
}
|
||
|
||
function makeStrategies(): TkPromptStrategy[] {
|
||
return [
|
||
{
|
||
slot: 'safe',
|
||
labelZh: '最稳',
|
||
objective: 'conversion',
|
||
styleForm: 'scene',
|
||
style: 'scene',
|
||
videoMode: 'scene_demo',
|
||
hookPatternId: 'pain_strike',
|
||
firstFrameId: 'pain_reveal',
|
||
firstFrame: '首帧呈现真实不便',
|
||
openingAction: '手部立即指向可见结构',
|
||
openingLine: 'This detail fixes the awkward moment.',
|
||
relevanceBeat: '真实台面中的不便与商品动作连续发生',
|
||
productBridge: '沿同一动作进入商品结构自证',
|
||
buyingMotivationId: 'problem_relief',
|
||
buyingMotivation: '缓解真实使用不便',
|
||
proofMethodId: 'visible_operation',
|
||
proofMethod: '现场完成一次可见操作',
|
||
ctaStrategy: '邀请查看当前商品详情',
|
||
allowedFactIds: ['visible-001', 'confirmed-002'],
|
||
requiredMaterials: [],
|
||
},
|
||
{
|
||
slot: 'native',
|
||
labelZh: '最原生',
|
||
objective: 'trust',
|
||
styleForm: 'product',
|
||
style: 'product_pov',
|
||
videoMode: 'product_closeup',
|
||
hookPatternId: 'sound_hook',
|
||
firstFrameId: 'sound_sync',
|
||
firstFrame: '商品操作拟音与微距动作同步',
|
||
openingAction: '手部按下可见结构并触发卡扣声',
|
||
openingLine: 'Listen to this tiny detail.',
|
||
relevanceBeat: '熟悉的操作声音对应真实使用瞬间',
|
||
productBridge: '音画同步落到结构特写',
|
||
buyingMotivationId: 'confidence',
|
||
buyingMotivation: '通过可见细节建立使用信心',
|
||
proofMethodId: 'structure_closeup',
|
||
proofMethod: '微距展示结构和操作关系',
|
||
ctaStrategy: '邀请用户近看商品细节',
|
||
allowedFactIds: ['visible-001'],
|
||
requiredMaterials: [],
|
||
},
|
||
{
|
||
slot: 'creative',
|
||
labelZh: '最抓眼',
|
||
objective: 'attention',
|
||
styleForm: 'product',
|
||
style: 'unboxing',
|
||
videoMode: 'product_closeup',
|
||
hookPatternId: 'visual_shock',
|
||
firstFrameId: 'unboxing_reveal',
|
||
firstFrame: '首帧以开启动作揭晓商品结构',
|
||
openingAction: '手部打开完整封装并连续移向结构',
|
||
openingLine: 'Wait until you see this part.',
|
||
relevanceBeat: '揭晓动作立刻说明结构与使用场景的关系',
|
||
productBridge: '继续同一开启动作完成场景证明',
|
||
buyingMotivationId: 'curiosity',
|
||
buyingMotivation: '通过结构揭晓满足好奇心',
|
||
proofMethodId: 'context_demo',
|
||
proofMethod: '在真实台面完成场景演示',
|
||
ctaStrategy: '邀请查看商品完整结构',
|
||
allowedFactIds: ['visible-001', 'confirmed-002'],
|
||
requiredMaterials: ['已确认封装'],
|
||
},
|
||
];
|
||
}
|
||
|
||
function makeCandidate(duration: 5 | 10 | 15): TkPromptOptionCandidate {
|
||
const timeline = duration === 5
|
||
? [[0, 1, 'stop'], [1, 3, 'relevance'], [3, 5, 'proof']] as const
|
||
: duration === 10
|
||
? [[0, 1, 'stop'], [1, 3, 'relevance'], [3, 7, 'proof'], [7, 10, 'cta']] as const
|
||
: [[0, 1, 'stop'], [1, 3, 'relevance'], [3, 7, 'proof'], [7, 11, 'resolution'], [11, 15, 'cta']] as const;
|
||
return {
|
||
title: '商品结构自证',
|
||
whyItFits: '使用可见结构和真实操作建立信任',
|
||
coreInformationPoints: [{
|
||
messageId: 'message-001',
|
||
message: '商品表面有可见网格结构',
|
||
factIds: ['visible-001'],
|
||
}],
|
||
primaryActionId: duration === 5 ? 'action-001' : null,
|
||
beats: timeline.map(([startSeconds, endSeconds], index) => ({
|
||
startSeconds,
|
||
endSeconds,
|
||
environment: '真实家居台面',
|
||
subject: '商品可见结构与操作手部',
|
||
action: '手部指向并按下可见结构',
|
||
shotSize: '微距特写',
|
||
coreInformationPointIds: ['message-001'],
|
||
actionPhase: duration === 5 ? (['start', 'progress', 'complete'] as const)[index]! : null,
|
||
productImageIndex: 1,
|
||
presenterVisible: false,
|
||
movementId: 'slow_push_in',
|
||
lighting: '柔和真实室内光',
|
||
editingCue: '在手指按下时切镜',
|
||
soundDesign: '真实按键卡扣声与轻微环境声',
|
||
voiceoverTarget: duration === 5 ? '' : 'See this detail.',
|
||
voiceoverZh: duration === 5 ? '' : '看清这个细节。',
|
||
factIds: ['visible-001'],
|
||
})),
|
||
assumptions: [],
|
||
requiredMaterials: [],
|
||
warnings: [],
|
||
};
|
||
}
|