1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-20 09:54:17 +08:00
parent 67c584b4df
commit 50759f3c1e
159 changed files with 16938 additions and 16925 deletions

View File

@@ -1,629 +1 @@
export type QwenSpriteActionTemplateId =
| 'idle'
| 'run'
| 'attack_slash'
| 'hurt'
| 'die';
export type QwenSpriteActionTemplate = {
id: QwenSpriteActionTemplateId;
label: string;
loop: boolean;
defaultFps: number;
bodyTravel: string;
weaponRule: string;
stagingDirection?: string;
defaultDetailText?: string;
sequenceLines: [string, string, string, string];
ending: string;
};
export const DEFAULT_MASTER_NEGATIVE_PROMPT =
'正面视角,左朝向,完全 90 度纯右视图镜头透视半身像脚被裁切头顶被裁切多角色复杂背景建筑场景道具堆叠漂浮物烟雾环境武器消失武器换手额外手臂额外腿服装变化脸部变化模糊运动模糊文字水印UI 元素';
export const DEFAULT_SHEET_NEGATIVE_PROMPT =
'多角色左右朝向混乱前视图背视图镜头切换景别变化特写脚底裁切头顶裁切缺手缺脚额外肢体武器消失武器换手服装变化脸部变化发型变化动作不连续重复帧过多构图混乱背景复杂强透视运动模糊残影文字水印UI边框覆盖角色';
export const DEFAULT_REPAIR_NEGATIVE_PROMPT =
'多角色,错误朝向,缺手,缺脚,额外肢体,武器消失,武器换手,脸部变化,发型变化,服装变化,模糊,运动模糊,复杂背景,文字,水印';
const CHIBI_STYLE_TEXT =
'Q版大头身动作角色头身比固定控制在 2 到 3 头身,头部占比明显更大,类似横版像素 RPG 可扮演角色的比例,不要写实长身比例。';
const PIXEL_STYLE_TEXT =
'参考项目内可扮演角色的像素风动作角色画风,整体是像素游戏角色设计方向,身体始终朝右,适合横版动作 sprite 资产。';
const SIDE_FACING_RIGHT_TEXT =
'角色采用横版动作素材常用的右向斜侧身站姿,身体整体朝右,但保留少量正面信息,能读到面部轮廓与胸肩结构,不是完全 90 度纯右视图,也不是正面立绘。';
const SUBJECT_ONLY_TEXT =
'画面中只保留单个角色主体,不要额外人物、动物、召唤物、载具或陪体。';
const CLEAN_BACKGROUND_TEXT =
'背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素或其他角色以外的场景内容。';
const STYLE_REFERENCE_SCOPE_TEXT =
'参考图不仅用于约束像素画风、颜色组织、头身比例、右朝向和镜头语言,还要严格约束身体结构骨架。生成结果必须优先沿用参考图的人形动作角色身体结构,包括头、躯干、双臂、双腿、站姿重心和整体头身比;可以变化发型、服装、主题配饰和材质,但不要脱离参考图的人形身体结构。';
const CONCEPT_INTERPRETATION_TEXT =
'请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先生成人形拟人化角色,让主题词主要体现在服装、头饰、冠冕、权杖、纹样、材质和发光装饰上。';
const HUMANLIKE_PRIORITY_TEXT =
'默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,这样更适合横版动作 sprite。只有当文字设定明确要求鱼尾、触手身体、伞盖头部、无双腿、漂浮体、凝胶体或其他非人结构时才改为对应非人身体。';
const CONCEPT_HIERARCHY_TEXT =
'视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。';
const THEME_APPLICATION_BOUNDARY_TEXT =
'主题词默认只作用在角色自身的服装剪裁、材质、纹样、饰品、武器和发光细节上,不要把主题词自动扩写成背景建筑、自然场景、漂浮装饰或额外环境物件。';
const CHIBI_CHARACTER_TEXT =
'即使角色带有怪物或海洋主题,也优先做成 Q版可爱的人形动作角色方便读图和后续动画化。';
const SETTING_AND_ROLE_ALIGNMENT_TEXT =
'先从角色设定中提炼世界设定、时代氛围、阵营身份、职业职责、战斗习惯、装备结构、材质配色和情绪气质,再把这些信息落实到发型、服装层次、护具、武器、饰品、轮廓和动作节奏里,不要混入与设定无关的现代服饰、写实摄影镜头、枪械体系或其他世界观元素。';
const CHARACTER_DETAIL_COVERAGE_TEXT =
'角色描述需要尽量落到可视化细节:发型与脸部识别点、年龄层与气质、服装层次、护具与配饰、武器/法器类型、主色与材质、职业姿态与世界观痕迹,避免只有抽象身份词。';
export const DEFAULT_CHARACTER_BRIEF =
'魔潮复苏边境城邦中的少女遗迹冒险者Q版大头身约 2 到 3 头身,金棕色微卷短发,琥珀色眼睛,额前碎发与侧边发束清晰,表情明亮但带警觉;穿分层轻甲、短披风和旅行短裙,皮革护腕、金属护膝、系带短靴完整可见;腰间挂药剂包、地图筒与小型护符,右手持单手短剑,主色为蜂蜜金、墨绿和旧银,轮廓利落,像长期在遗迹与荒野间行动的年轻探索者。';
export const PLAYABLE_CHARACTER_STYLE_REFERENCE_SOURCES = [
'/character/Sword Princess/Original/Hero/idle/Idle01.png',
'/character/Archer Hero/Original/Hero/idle/idle01.png',
'/character/Girl Hero 1/Original/Hero/Idle/Idle01.png',
'/character/Punch Hero 3/Original/Hero/Idle/Idle01.png',
'/character/Fighter 4/original/Hero/idle/idle01.png',
];
export const QWEN_SPRITE_ACTION_TEMPLATES: QwenSpriteActionTemplate[] = [
{
id: 'idle',
label: '待机循环',
loop: true,
defaultFps: 8,
bodyTravel: '原地',
weaponRule: '武器始终在主手,位置稳定',
sequenceLines: [
'1-4 帧:稳定站姿,轻微呼吸起伏',
'5-8 帧:胸腔与肩膀轻微抬起,衣摆极轻微变化',
'9-12 帧:呼气回落,重心恢复',
'13-16 帧:逐渐回到与首帧接近的站姿',
],
ending: '第 16 帧自然衔接第 1 帧',
},
{
id: 'run',
label: '奔跑循环',
loop: true,
defaultFps: 12,
bodyTravel: '小幅前移但角色中心基本固定',
weaponRule: '武器始终在主手,不换手',
sequenceLines: [
'1-4 帧:右腿前摆,左腿后蹬,身体略前倾',
'5-8 帧:双腿交叉经过身体下方,手臂反向摆动',
'9-12 帧:左腿前摆,右腿后蹬,继续前倾',
'13-16 帧:完成另一半跑步循环并回到可接第 1 帧的状态',
],
ending: '第 16 帧能无缝接回第 1 帧',
},
{
id: 'attack_slash',
label: '横斩攻击',
loop: false,
defaultFps: 12,
bodyTravel: '中幅前探',
weaponRule: '右手持武器,始终右手,不换手',
sequenceLines: [
'1-4 帧:轻微收身蓄力,武器向后收',
'5-8 帧:重心前压,挥击开始',
'9-12 帧:斩击达到最大幅度,动作力量最强',
'13-16 帧:顺势收招,回到可接下一动作的稳定姿态',
],
ending: '第 16 帧停在收招后稳定姿态',
},
{
id: 'hurt',
label: '受击后仰',
loop: false,
defaultFps: 10,
bodyTravel: '原地或极小后仰',
weaponRule: '武器不要脱手,不要换手',
sequenceLines: [
'1-4 帧:突然受击,头肩后仰',
'5-8 帧:身体失衡最明显',
'9-12 帧:手臂和武器随惯性摆动',
'13-16 帧:逐渐恢复到勉强站稳的姿态',
],
ending: '第 16 帧能接回 idle 或下一个动作',
},
{
id: 'die',
label: '倒地死亡',
loop: false,
defaultFps: 8,
bodyTravel: '明显倒地位移',
weaponRule: '武器不可瞬间消失',
sequenceLines: [
'1-4 帧:受创失衡,重心被打断',
'5-8 帧:身体明显下坠或后仰',
'9-12 帧:倒地过程完成,动作幅度最大',
'13-16 帧:停在清晰的终止姿态',
],
ending: '第 16 帧停在死亡结束姿态,不需要循环',
},
];
const ACTION_TEMPLATE_DETAILS: Record<
QwenSpriteActionTemplateId,
{ stagingDirection: string; defaultDetailText: string }
> = {
idle: {
stagingDirection:
'演出重点是轻呼吸、微幅重心起伏、戒备感和可循环衔接。',
defaultDetailText:
'保持与角色设定一致的戒备气质和职业站姿,重心稳在脚下,呼吸起伏轻微,披风、衣摆、发梢和小型饰品只有细小摆动,武器安静跟随身体微动,像角色在所属世界里随时准备探索、交涉或开战的待机瞬间。',
},
run: {
stagingDirection:
'演出重点是持续推进、步点清楚、上身稳定和装备惯性。',
defaultDetailText:
'跑动时要体现角色职业和世界设定中的装备重量感,身体略微前压,步幅清晰,手臂与武器顺势摆动,披风、衣摆和挂件跟着惯性后甩,像角色正穿过战场、街巷或遗迹通道迅速移动,节奏连续稳定。',
},
attack_slash: {
stagingDirection:
'演出重点是收身蓄力、爆发斩击、武器轨迹和收招稳定。',
defaultDetailText:
'攻击前先有短暂收身蓄力,再顺着主手武器做干净利落的前踏斩击,爆发点明确,武器轨迹贴合角色职业习惯与世界观武器设定,衣摆和挂件随发力甩动,收招后还能稳住重心,像久经战斗训练的真实角色动作。',
},
hurt: {
stagingDirection:
'演出重点是冲击反馈、短暂失衡、惯性摆动和重新稳住。',
defaultDetailText:
'受击瞬间要有明确冲击反馈,肩背后仰或身体侧偏,表情短促吃痛但不夸张,武器和手臂因惯性发生合理摆动,服装层次和挂件跟着抖动一下,随后努力稳住姿态,像这个角色在其世界观里真实挨到一击后的反应。',
},
die: {
stagingDirection:
'演出重点是失衡下坠、动作逐渐停尽和终止姿态清晰。',
defaultDetailText:
'死亡动作要先表现失衡与力量被抽离,再完成明显倒下或瘫落,武器和四肢随惯性下坠,披风、衣摆和饰品有最后一次明显摆动,最终停在清晰的终止姿态,符合角色身份、装备重量和世界氛围,不要轻飘或喜剧化。',
},
};
export function getActionTemplateById(id: QwenSpriteActionTemplateId) {
const template =
QWEN_SPRITE_ACTION_TEMPLATES.find((candidate) => candidate.id === id) ??
QWEN_SPRITE_ACTION_TEMPLATES[0];
return {
...template,
...ACTION_TEMPLATE_DETAILS[template.id],
};
}
export function readFileAsDataUrl(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result ?? ''));
reader.onerror = () => reject(reader.error ?? new Error('读取文件失败'));
reader.readAsDataURL(file);
});
}
function loadImageFromSource(source: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`加载图片失败:${source}`));
image.src = source;
});
}
function drawContainedImage(
context: CanvasRenderingContext2D,
image: HTMLImageElement,
options: {
x: number;
y: number;
width: number;
height: number;
},
) {
const fitScale = Math.min(
options.width / image.width,
options.height / image.height,
);
const drawWidth = image.width * fitScale;
const drawHeight = image.height * fitScale;
const drawX = options.x + (options.width - drawWidth) / 2;
const drawY = options.y + (options.height - drawHeight) / 2;
context.drawImage(image, drawX, drawY, drawWidth, drawHeight);
}
export async function sliceSpriteSheetFrames(
spriteSource: string,
options: {
rows: number;
cols: number;
},
) {
const image = await loadImageFromSource(spriteSource);
const frameWidth = Math.floor(image.width / options.cols);
const frameHeight = Math.floor(image.height / options.rows);
const frames: string[] = [];
for (let rowIndex = 0; rowIndex < options.rows; rowIndex += 1) {
for (let colIndex = 0; colIndex < options.cols; colIndex += 1) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法创建画布上下文');
}
canvas.width = frameWidth;
canvas.height = frameHeight;
context.drawImage(
image,
colIndex * frameWidth,
rowIndex * frameHeight,
frameWidth,
frameHeight,
0,
0,
frameWidth,
frameHeight,
);
frames.push(canvas.toDataURL('image/png'));
}
}
return {
frameWidth,
frameHeight,
frames,
width: image.width,
height: image.height,
};
}
export async function extractSpriteFrame(
spriteSource: string,
options: {
rows: number;
cols: number;
frameIndex: number;
outputSize?: number;
},
) {
const sliced = await sliceSpriteSheetFrames(spriteSource, {
rows: options.rows,
cols: options.cols,
});
const frameSource = sliced.frames[options.frameIndex];
if (!frameSource) {
throw new Error('帧索引超出范围。');
}
if (!options.outputSize) {
return frameSource;
}
const image = await loadImageFromSource(frameSource);
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法创建画布上下文');
}
canvas.width = options.outputSize;
canvas.height = options.outputSize;
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(image, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/png');
}
export async function replaceSpriteFrame(
spriteSource: string,
options: {
rows: number;
cols: number;
frameIndex: number;
replacementSource: string;
},
) {
const spriteImage = await loadImageFromSource(spriteSource);
const replacementImage = await loadImageFromSource(options.replacementSource);
const frameWidth = Math.floor(spriteImage.width / options.cols);
const frameHeight = Math.floor(spriteImage.height / options.rows);
const rowIndex = Math.floor(options.frameIndex / options.cols);
const colIndex = options.frameIndex % options.cols;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法创建画布上下文');
}
canvas.width = spriteImage.width;
canvas.height = spriteImage.height;
context.drawImage(spriteImage, 0, 0);
context.clearRect(
colIndex * frameWidth,
rowIndex * frameHeight,
frameWidth,
frameHeight,
);
context.drawImage(
replacementImage,
colIndex * frameWidth,
rowIndex * frameHeight,
frameWidth,
frameHeight,
);
return canvas.toDataURL('image/png');
}
export function buildOrderedActiveFrameIndices(
frameOrder: number[],
activeFrames: number[],
) {
return frameOrder.filter((frameIndex) => activeFrames.includes(frameIndex));
}
export function buildOrderedActiveFrameSources(
frameDataUrls: string[],
frameOrder: number[],
activeFrames: number[],
) {
return buildOrderedActiveFrameIndices(frameOrder, activeFrames)
.map((frameIndex) => frameDataUrls[frameIndex] ?? '')
.filter(Boolean);
}
export async function composeSpriteSheetFromFrames(
frameSources: string[],
options: {
cols: number;
rows?: number;
frameWidth?: number;
frameHeight?: number;
padToGrid?: boolean;
},
) {
if (frameSources.length === 0) {
throw new Error('没有可用于拼接精灵表的帧。');
}
const images = await Promise.all(
frameSources.map((source) => loadImageFromSource(source)),
);
const frameWidth =
options.frameWidth ??
Math.max(...images.map((image) => image.width), 1);
const frameHeight =
options.frameHeight ??
Math.max(...images.map((image) => image.height), 1);
const rows =
options.rows ?? Math.max(1, Math.ceil(images.length / options.cols));
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法创建画布上下文');
}
canvas.width = frameWidth * options.cols;
canvas.height = frameHeight * rows;
context.clearRect(0, 0, canvas.width, canvas.height);
const totalCells = options.padToGrid ? rows * options.cols : images.length;
for (let index = 0; index < totalCells; index += 1) {
const image = images[index];
if (!image) {
continue;
}
const rowIndex = Math.floor(index / options.cols);
const colIndex = index % options.cols;
drawContainedImage(context, image, {
x: colIndex * frameWidth,
y: rowIndex * frameHeight,
width: frameWidth,
height: frameHeight,
});
}
return {
dataUrl: canvas.toDataURL('image/png'),
rows,
cols: options.cols,
frameWidth,
frameHeight,
frameCount: frameSources.length,
};
}
export async function buildPlayableCharacterStyleReferenceBoard(
sources = PLAYABLE_CHARACTER_STYLE_REFERENCE_SOURCES,
) {
const images = await Promise.all(
sources.map((source) => loadImageFromSource(source)),
);
const cols = 3;
const rows = 2;
const cellSize = 320;
const padding = 24;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('无法创建画布上下文');
}
canvas.width = cols * cellSize + padding * 2;
canvas.height = rows * cellSize + padding * 2;
context.fillStyle = '#f6f0dd';
context.fillRect(0, 0, canvas.width, canvas.height);
context.imageSmoothingEnabled = false;
images.forEach((image, index) => {
const colIndex = index % cols;
const rowIndex = Math.floor(index / cols);
drawContainedImage(context, image, {
x: padding + colIndex * cellSize,
y: padding + rowIndex * cellSize,
width: cellSize,
height: cellSize,
});
});
return canvas.toDataURL('image/png');
}
export function buildMasterPrompt(characterBrief: string) {
return [
'???2D ???????????????????????????????????????????? sprite sheet ???',
`?????${SIDE_FACING_RIGHT_TEXT}`,
`?????${SUBJECT_ONLY_TEXT}`,
`?????1:1 ??????????????????????????????????????????????????${CLEAN_BACKGROUND_TEXT}`,
`?????${CHIBI_STYLE_TEXT} ${CHIBI_CHARACTER_TEXT} ${PIXEL_STYLE_TEXT} ${STYLE_REFERENCE_SCOPE_TEXT} ?????????????????????????????????????/??/???????????????????????`,
SETTING_AND_ROLE_ALIGNMENT_TEXT,
CHARACTER_DETAIL_COVERAGE_TEXT,
CONCEPT_INTERPRETATION_TEXT,
HUMANLIKE_PRIORITY_TEXT,
CONCEPT_HIERARCHY_TEXT,
THEME_APPLICATION_BOUNDARY_TEXT,
characterBrief.trim(),
]
.filter(Boolean)
.join('\n');
}
export function buildSheetPrompt(options: {
characterBrief: string;
actionTemplate: QwenSpriteActionTemplate;
extraDirection: string;
}) {
return [
`???1??????????? 4x4 ? sprite sheet?? 16 ????????????????????????????????????????????????????????????????????????????????????????????????????? 90 ??????${CHIBI_STYLE_TEXT} ${CHIBI_CHARACTER_TEXT} ${PIXEL_STYLE_TEXT} ${STYLE_REFERENCE_SCOPE_TEXT}`,
SETTING_AND_ROLE_ALIGNMENT_TEXT,
CONCEPT_INTERPRETATION_TEXT,
HUMANLIKE_PRIORITY_TEXT,
CONCEPT_HIERARCHY_TEXT,
THEME_APPLICATION_BOUNDARY_TEXT,
`????${options.actionTemplate.label}`,
`???????${options.actionTemplate.stagingDirection ?? ACTION_TEMPLATE_DETAILS[options.actionTemplate.id].stagingDirection}`,
`?????${options.actionTemplate.loop ? '?' : '?'}`,
`?????${options.actionTemplate.bodyTravel}`,
`?????${options.actionTemplate.weaponRule}`,
...options.actionTemplate.sequenceLines,
`?????${options.actionTemplate.ending}`,
'?????????????????????????????????????????????????????????????????????????????????? sprite frames?',
options.characterBrief.trim(),
`???????${options.extraDirection.trim() || options.actionTemplate.defaultDetailText || ACTION_TEMPLATE_DETAILS[options.actionTemplate.id].defaultDetailText}`,
]
.filter(Boolean)
.join('\n');
}
export function buildRepairPrompt(options: {
issueText: string;
useNeighborLabel: '???' | '???';
}) {
return [
`???1??????????2??????????3???????2??${options.useNeighborLabel}?`,
`?????????????????????????????????????????????????????????2???????1?????????????? 90 ??????${CHIBI_STYLE_TEXT} ${CHIBI_CHARACTER_TEXT} ${PIXEL_STYLE_TEXT} ${STYLE_REFERENCE_SCOPE_TEXT} ${SETTING_AND_ROLE_ALIGNMENT_TEXT} ${CONCEPT_INTERPRETATION_TEXT} ${HUMANLIKE_PRIORITY_TEXT} ${CONCEPT_HIERARCHY_TEXT} ${THEME_APPLICATION_BOUNDARY_TEXT} ???3???????????????? sprite sheet ??`,
'?????????????????????????',
`?????${options.issueText.trim() || '????????????????????'}`,
].join('\n');
}
export function buildVideoActionPrompt(options: {
actionTemplate: QwenSpriteActionTemplate;
actionDetailText: string;
useChromaKey: boolean;
characterBrief: string;
}) {
return [
`???????????????? ${options.actionTemplate.label}?`,
`??????1??????????????????????????????????? 90 ??????${CHIBI_STYLE_TEXT} ${CHIBI_CHARACTER_TEXT} ${PIXEL_STYLE_TEXT} ${STYLE_REFERENCE_SCOPE_TEXT}`,
SETTING_AND_ROLE_ALIGNMENT_TEXT,
CONCEPT_INTERPRETATION_TEXT,
HUMANLIKE_PRIORITY_TEXT,
CONCEPT_HIERARCHY_TEXT,
THEME_APPLICATION_BOUNDARY_TEXT,
`???????${options.actionTemplate.stagingDirection ?? ACTION_TEMPLATE_DETAILS[options.actionTemplate.id].stagingDirection}`,
`?????${options.actionTemplate.sequenceLines.join('?')}??????${options.actionTemplate.ending}?`,
options.useChromaKey
? '??????????????????????????????'
: '?????????????',
`???????${options.actionDetailText.trim() || options.actionTemplate.defaultDetailText || ACTION_TEMPLATE_DETAILS[options.actionTemplate.id].defaultDetailText}`,
`?????${options.characterBrief.trim()}`,
'?????????????????????????????????????????',
].join(' ');
}
export async function triggerDataUrlDownload(
filename: string,
dataUrl: string,
) {
const response = await fetch(dataUrl);
const blob = await response.blob();
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = objectUrl;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(objectUrl);
}
export function triggerJsonDownload(filename: string, value: unknown) {
const blob = new Blob([JSON.stringify(value, null, 2)], {
type: 'application/json',
});
const objectUrl = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = objectUrl;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(objectUrl);
}
export function buildDefaultFrameOrder(frameCount: number) {
return Array.from({ length: frameCount }, (_, index) => index);
}
export function restoreAllFrames(frameCount: number) {
return buildDefaultFrameOrder(frameCount);
}
export function buildMasterNegativePrompt(_characterBrief: string) {
return `${DEFAULT_MASTER_NEGATIVE_PROMPT},不要机械地把主题词直接画成完整怪物本体,也不要把主题词自动扩写成角色以外的场景元素,除非文字设定明确要求`;
}
export function buildSheetNegativePrompt(_characterBrief: string) {
return `${DEFAULT_SHEET_NEGATIVE_PROMPT},不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体`;
}
export function buildRepairNegativePrompt(_characterBrief: string) {
return `${DEFAULT_REPAIR_NEGATIVE_PROMPT},不要机械地把主题词直接画成完整怪物本体,除非文字设定明确要求非人身体`;
}
export function moveFrameOrderItem(
frameOrder: number[],
frameIndex: number,
direction: -1 | 1,
) {
const currentOrderIndex = frameOrder.indexOf(frameIndex);
if (currentOrderIndex < 0) {
return frameOrder;
}
const targetIndex = currentOrderIndex + direction;
if (targetIndex < 0 || targetIndex >= frameOrder.length) {
return frameOrder;
}
const nextOrder = [...frameOrder];
const [item] = nextOrder.splice(currentOrderIndex, 1);
nextOrder.splice(targetIndex, 0, item);
return nextOrder;
}
export function toggleActiveFrame(activeFrames: number[], frameIndex: number) {
if (activeFrames.includes(frameIndex)) {
return activeFrames.filter((item) => item !== frameIndex);
}
return [...activeFrames, frameIndex].sort((left, right) => left - right);
}
export * from '../prompts/qwenSpriteSheetToolPrompts';