大量修改二期功能清单内容
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/** 脚本设定卡的选项体系:表现形式 × 视频结构 × 时长(二期第2段)。
|
||||
*
|
||||
* 这里的枚举、禁用组合、时长区间必须跟后端 apps/ai/script_agent.py 的常量保持一致 ——
|
||||
* 后端会 coerce 兜底,但前端先挡住能避免用户选了一个又被悄悄改掉。
|
||||
*/
|
||||
|
||||
export type PresentationFormat = "oral" | "drama" | "vlog";
|
||||
export type VideoStructure = "pain" | "contrast" | "review" | "scene";
|
||||
|
||||
export const PRESENTATION_FORMATS: Record<PresentationFormat, string> = {
|
||||
oral: "口播",
|
||||
drama: "短剧",
|
||||
vlog: "Vlog",
|
||||
};
|
||||
|
||||
export const VIDEO_STRUCTURES: Record<VideoStructure, string> = {
|
||||
pain: "痛点解决",
|
||||
contrast: "前后对比",
|
||||
review: "测评验证",
|
||||
scene: "场景种草",
|
||||
};
|
||||
|
||||
export const PRESENTATION_KEYS = Object.keys(PRESENTATION_FORMATS) as PresentationFormat[];
|
||||
export const STRUCTURE_KEYS = Object.keys(VIDEO_STRUCTURES) as VideoStructure[];
|
||||
|
||||
/** 一句话说清每个选项是什么,挂在下拉旁边当说明,别让用户猜。 */
|
||||
export const PRESENTATION_HINT: Record<PresentationFormat, string> = {
|
||||
oral: "一个人对镜头说,信息密度最高、最省制作",
|
||||
drama: "有角色有冲突的微型剧,完播率最高",
|
||||
vlog: "第一人称生活记录,广告感最低",
|
||||
};
|
||||
|
||||
export const STRUCTURE_HINT: Record<VideoStructure, string> = {
|
||||
pain: "先戳痛点再给解法,适用面最广",
|
||||
contrast: "用前后差距说话,视觉冲击最强",
|
||||
review: "当场举证验证,适合高客单价",
|
||||
scene: "把商品放进向往的场景里种草",
|
||||
};
|
||||
|
||||
/** 唯一禁用组合:短剧 × 测评验证 —— 演出来的实测没有可信度。 */
|
||||
const FORBIDDEN: ReadonlyArray<[PresentationFormat, VideoStructure]> = [["drama", "review"]];
|
||||
|
||||
export function isForbidden(format: PresentationFormat, structure: VideoStructure): boolean {
|
||||
return FORBIDDEN.some(([f, s]) => f === format && s === structure);
|
||||
}
|
||||
|
||||
/** 1.8 组合联动:选定表现形式后,视频结构只剩这些可选。 */
|
||||
export function allowedStructures(format: PresentationFormat): VideoStructure[] {
|
||||
return STRUCTURE_KEYS.filter((key) => !isForbidden(format, key));
|
||||
}
|
||||
|
||||
// ── 时长:总时长 5–60 秒、5 秒步进;单镜 4–15 秒由后端切,前端只管总时长 ──
|
||||
export const SEGMENT_DURATION_MIN = 4;
|
||||
export const SEGMENT_DURATION_MAX = 15;
|
||||
export const TOTAL_DURATION_MIN = 5;
|
||||
export const TOTAL_DURATION_MAX = 60;
|
||||
export const TOTAL_DURATION_STEP = 5;
|
||||
export const DURATION_OPTIONS: number[] = Array.from(
|
||||
{ length: (TOTAL_DURATION_MAX - TOTAL_DURATION_MIN) / TOTAL_DURATION_STEP + 1 },
|
||||
(_, i) => TOTAL_DURATION_MIN + i * TOTAL_DURATION_STEP
|
||||
);
|
||||
|
||||
/** 表现形式推荐的默认总时长:口播短平快 / 短剧要装下三幕 / Vlog 要铺氛围。 */
|
||||
const FORMAT_DEFAULT_DURATION: Record<PresentationFormat, number> = { oral: 30, drama: 45, vlog: 30 };
|
||||
/** 各结构能压到的最短总时长 —— 低于它证据或氛围就不成立了。 */
|
||||
const STRUCTURE_MIN_DURATION: Record<VideoStructure, number> = { pain: 15, contrast: 10, review: 20, scene: 20 };
|
||||
|
||||
export function recommendDuration(format: PresentationFormat, structure: VideoStructure): number {
|
||||
return Math.max(FORMAT_DEFAULT_DURATION[format], STRUCTURE_MIN_DURATION[structure]);
|
||||
}
|
||||
|
||||
export function clampDuration(seconds: number): number {
|
||||
if (!Number.isFinite(seconds)) return FORMAT_DEFAULT_DURATION.oral;
|
||||
const stepped = Math.round(seconds / TOTAL_DURATION_STEP) * TOTAL_DURATION_STEP;
|
||||
return Math.min(TOTAL_DURATION_MAX, Math.max(TOTAL_DURATION_MIN, stepped));
|
||||
}
|
||||
|
||||
/** 时长低于该结构的最短可用值时给一句提醒(不拦,只提示)。 */
|
||||
export function durationWarning(structure: VideoStructure, seconds: number): string {
|
||||
const min = STRUCTURE_MIN_DURATION[structure];
|
||||
if (seconds >= min) return "";
|
||||
return `${VIDEO_STRUCTURES[structure]}低于 ${min} 秒会压不住,建议调长`;
|
||||
}
|
||||
|
||||
// ── 2.3 按商品信息推荐默认组合 ──
|
||||
|
||||
type Combo = { format: PresentationFormat; structure: VideoStructure };
|
||||
|
||||
/** 品类关键词 → 推荐组合。命中第一条即用,没命中走兜底。 */
|
||||
const CATEGORY_RULES: ReadonlyArray<{ keywords: string[]; combo: Combo }> = [
|
||||
{ keywords: ["美妆", "护肤", "彩妆", "面膜", "精华", "洗护", "个护"], combo: { format: "oral", structure: "contrast" } },
|
||||
{ keywords: ["保健", "营养", "膳食", "益生菌", "维生素"], combo: { format: "oral", structure: "review" } },
|
||||
{ keywords: ["数码", "3c", "电子", "电器", "手机", "耳机", "相机", "工具"], combo: { format: "oral", structure: "review" } },
|
||||
{ keywords: ["食品", "零食", "饮料", "咖啡", "茶", "生鲜", "酒"], combo: { format: "vlog", structure: "scene" } },
|
||||
{ keywords: ["服饰", "女装", "男装", "服装", "鞋", "包", "配饰", "内衣"], combo: { format: "vlog", structure: "scene" } },
|
||||
{ keywords: ["家居", "家纺", "收纳", "厨具", "清洁", "日用"], combo: { format: "drama", structure: "pain" } },
|
||||
{ keywords: ["母婴", "宝宝", "儿童", "宠物"], combo: { format: "drama", structure: "pain" } },
|
||||
{ keywords: ["户外", "露营", "运动", "健身"], combo: { format: "vlog", structure: "scene" } },
|
||||
];
|
||||
|
||||
const FALLBACK_COMBO: Combo = { format: "oral", structure: "pain" };
|
||||
|
||||
/** 按商品品类 + 人群推荐一组「表现形式 × 视频结构 × 时长」。纯启发式,用户随时可改。 */
|
||||
export function recommendSetup(input: { category?: string; targetAudience?: string; title?: string }): Combo & {
|
||||
duration: number;
|
||||
reason: string;
|
||||
} {
|
||||
const haystack = [input.category, input.title, input.targetAudience]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
|
||||
let combo = FALLBACK_COMBO;
|
||||
let reason = "商品信息不足,先给一组最通用的";
|
||||
for (const rule of CATEGORY_RULES) {
|
||||
const hit = rule.keywords.find((word) => haystack.includes(word.toLowerCase()));
|
||||
if (hit) {
|
||||
combo = rule.combo;
|
||||
reason = `按「${hit}」品类推荐`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 防御:规则表若被改出禁用组合,这里兜住
|
||||
const structure = isForbidden(combo.format, combo.structure)
|
||||
? allowedStructures(combo.format)[0]
|
||||
: combo.structure;
|
||||
return {
|
||||
format: combo.format,
|
||||
structure,
|
||||
duration: recommendDuration(combo.format, structure),
|
||||
reason,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user