修复音频生成面板布局
对齐生成游戏音效面板底部参数、模型与生成按钮布局。 固定展示音效 Vidu 模型和背景音乐 Suno 模型。 补充音频生成面板布局回归测试并更新设计文档。
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
- `生成图标素材`
|
||||
- `生成UI设计图`
|
||||
- `生成视频`
|
||||
- `生成游戏音效`
|
||||
- `生成游戏背景音乐`
|
||||
- `生成角色动作`
|
||||
- `快速编辑`
|
||||
|
||||
@@ -49,8 +51,8 @@
|
||||
```
|
||||
|
||||
- 面板内不显示 `画面比例`、`大小尺寸`、`模型` 等字段标题,只显示当前选择值。
|
||||
- 图片类面板把画面比例和大小尺寸合并成一个左下角选项框;视频面板把比例、时长、清晰度合并成一个左下角选项框;音效面板把 type 和 BPM 合并成一个无标题左下角选项框。
|
||||
- 模型选项框和生成按钮位于右下角。
|
||||
- 图片类面板把画面比例和大小尺寸合并成一个左下角选项框;视频面板把比例、时长、清晰度合并成一个左下角选项框;音效面板把 Vidu duration 合并成一个无标题左下角选项框。
|
||||
- 模型选项框和生成按钮位于右下角,模型胶囊紧贴生成按钮;音效固定显示 `Vidu`,背景音乐固定显示 `Suno`,两者均沿用模型按钮样式但不可点击。
|
||||
- 点击后以页面级浮层向上弹出独立选项面板;父级面板隐藏或销毁时,选项面板同步销毁。
|
||||
- 点击选项只更新字段和选中样式,不收起选项面板;再次点击当前参数按钮可收起。
|
||||
- 选项面板打开后,点击父级生成面板内任意非选项框、非触发按钮区域必须收起;点击选项面板自身仍保持打开,便于连续修改。
|
||||
@@ -58,7 +60,7 @@
|
||||
- 模型子面板每行固定一个模型,不用方框包裹模型名;模型名不换行,前置对应模型类型图标,选中项在模型名后用对号标记。
|
||||
- 比例选项卡片内展示对应比例的线框。
|
||||
- 父级面板、底部选项框、弹出子面板字号保持一致。
|
||||
- 底部组合值使用 `·` 分隔,例如 `16:9 · 4秒 · 480p`、`单次·120BPM`。
|
||||
- 底部组合值使用 `·` 分隔,例如 `16:9 · 4秒 · 480p`、`16:9 · 1K`。
|
||||
- 底部参数热区与生成按钮等高,默认不显示阴影;悬停显示轻量阴影;箭头默认向下,展开后旋转向上。
|
||||
- 底边栏中会在上方弹出二级选项的入口不再依赖点击展开。鼠标悬停到入口即可打开二级面板,鼠标离开入口和二级面板后自动收起;当前范围包括 `生成规范` 和 `生成音乐`。
|
||||
- 底边栏二级选项面板必须锚定到对应入口按钮本身,不使用屏幕居中或固定底部偏移;移动端窄屏下也应保持跟随入口位置。
|
||||
@@ -185,7 +187,8 @@
|
||||
- 新建空白待生成占位的尺寸必须和面板参数一致;图片类修改比例 / 尺寸、视频修改清晰度后,画布空白占位同步变更且保持中心点。
|
||||
- 点击角色图只选中图层并显示工具栏,不自动弹出重绘、快速编辑或角色动画面板;点击工具栏或右键菜单中的 `生成动画` 才创建角色动作占位和面板。
|
||||
- 点击 UI 设计图只选中图层并显示工具栏;工具栏在 `去除背景按钮` 后显示 `提取素材`,点击后固定用 `gpt-image-2` 和提示词 `提取画面中的所有独立并整理成spritesheet` 生成 spritesheet,再复用图标素材拆分流程,把 spritesheet 图集和拆分素材都放到画布。
|
||||
- 生成游戏音效面板底部不显示 `type` / `tempo` 字段标题,只有一个音效参数按钮;type 只支持 `one-shot` / `loop`,BPM 为 `1-300` 的数字或 `null`,选择后按钮显示 `单次·120BPM` 这类组合值。
|
||||
- 生成游戏音效面板底部不显示字段标题,左下角只有一个时长参数按钮,选项为 Vidu duration `2-10` 秒;右下角固定模型胶囊显示 `Vidu` 并紧贴生成按钮。
|
||||
- 生成游戏背景音乐面板右下角固定模型胶囊显示 `Suno` 并紧贴生成按钮;`make_instrumental` 不在 UI 中展示。
|
||||
- 生成视频结果以视频图层加入画布,画布媒体元素标记为 `画布视频:生成视频 N`。
|
||||
- 生成器输入、参数、参考图和占位框在刷新后仍存在;已生成对象的生成器面板继续跟随成品图层。
|
||||
- Seedance 2.0 参考视频 / 音频上传不产生 `data:video/*` / 大体积音频 JSON;提交请求优先携带 `objectKey`,后端 Ark body 含 `generate_audio:false` 且不含未确认的 `mode` 字段。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- `生成游戏音效`
|
||||
- `生成游戏背景音乐`
|
||||
3. 选择某一项后创建独立 `generation-dialog` 画布生成对象,并通过现有 placement 模型避让已有图层和占位。
|
||||
4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 生成按钮区,不写规则说明类文案。
|
||||
4. 面板 UI 复用 `生成角色形象` 的紧凑结构:上方为字段区,底部为参数 / 模型 / 生成按钮区,不写规则说明类文案。音效参数按钮靠左下角,固定模型胶囊紧贴生成按钮;背景音乐同样在右下角显示固定模型胶囊并紧贴生成按钮。
|
||||
5. 生成中隐藏设置面板,只保留画布中的音频生成占位;失败后恢复面板并展示短错误。
|
||||
|
||||
## 面板字段
|
||||
@@ -21,7 +21,7 @@
|
||||
### 生成游戏音效
|
||||
|
||||
- `prompt`:用户输入的音效提示词,直接作为 Vidu 文生音频请求的 `prompt` 字段。
|
||||
- `model`:固定默认 `audio1.0`,UI 显示为 `Vidu`;暂不展示 Suno 文生音效模型入口。
|
||||
- `model`:固定默认 `audio1.0`,UI 以禁用态模型胶囊显示为 `Vidu`,位置紧贴生成按钮;暂不展示 Suno 文生音效模型入口。
|
||||
- `duration`:Vidu 音频时长,UI 以一个无标题参数按钮展示,选项范围 `2-10` 秒,默认 `5` 秒。
|
||||
- 提交到 VectorEngine 时映射为 Vidu 文生音频字段:`model: "audio1.0"`、`prompt`、`duration`、可选 `seed`。当前编辑器音效固定使用 Vidu `audio1.0`,不再走 Suno `task: "sound"` / `metadata_params` 文生音效契约。
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
- `gpt_description_prompt`:用户输入的背景音乐提示词。
|
||||
- `make_instrumental`:固定传 `true`,不在 UI 中展示为可改字段。
|
||||
- 提交到 VectorEngine 时映射为 Suno 纯音乐模式字段:`mv`、`gpt_description_prompt`、`make_instrumental: true`。`mv` 后端固定使用默认 Suno 模型,不在 UI 中展示。
|
||||
- 提交到 VectorEngine 时映射为 Suno 纯音乐模式字段:`mv`、`gpt_description_prompt`、`make_instrumental: true`。`mv` 后端固定使用默认 Suno 模型,UI 以禁用态模型胶囊显示 `Suno`。
|
||||
- `gpt_description_prompt` 按 Apifox 契约限制 200 字,超出时由 BFF 返回参数错误。
|
||||
|
||||
## 画布数据
|
||||
@@ -113,10 +113,10 @@ POST /api/editor/audios/background-music/generations
|
||||
|
||||
- 底部工具栏显示 `生成音乐`。
|
||||
- 点击 `生成音乐` 只出现选项框,不立刻创建占位。
|
||||
- 点击 `生成游戏音效` 后出现音效面板,文本字段为 `prompt`;底部只有一个无标题时长参数按钮。
|
||||
- 点击 `生成游戏音效` 后出现音效面板,文本字段为 `prompt`;底部左侧只有一个无标题时长参数按钮,右侧为固定 `Vidu` 模型胶囊和生成按钮。
|
||||
- 音效时长只提供 `2-10` 秒选项,默认 `5` 秒,提交到 BFF 的字段为 `duration`。
|
||||
- 音效面板模型显示 `Vidu`,提交 `model: "audio1.0"`;不显示 Suno 文生音效模型或 Suno 音效入口。
|
||||
- 点击 `生成游戏背景音乐` 后出现背景音乐面板,字段为 `gpt_description_prompt`,不展示 `make_instrumental`。
|
||||
- 点击 `生成游戏背景音乐` 后出现背景音乐面板,字段为 `gpt_description_prompt`,右侧固定显示 `Suno` 模型胶囊,不展示 `make_instrumental`。
|
||||
- 音效提交到 `/api/editor/audios/sound-effects/generations`,背景音乐提交到 `/api/editor/audios/background-music/generations`。
|
||||
- 成功后画布新增音频卡,能通过 `<audio controls>` 播放。
|
||||
- 成功后的音频卡展示提示词;卡片中央按未悬停图标、悬停播放、播放中暂停切换,播放控件固定在卡片底部。
|
||||
|
||||
@@ -517,8 +517,25 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
render(<AudioHarness />);
|
||||
|
||||
const panel = screen.getByRole('dialog', { name: '生成游戏音效' });
|
||||
const footer = panel.querySelector(
|
||||
'.image-canvas-editor__generation-composer-footer',
|
||||
);
|
||||
expect(footer).toBeTruthy();
|
||||
expect(footer?.children[0]?.className).toContain(
|
||||
'image-canvas-editor__option-popover-anchor--dimensions',
|
||||
);
|
||||
expect(footer?.children[1]?.className).toContain(
|
||||
'image-canvas-editor__option-popover-anchor--model',
|
||||
);
|
||||
expect(footer?.children[2]?.className).toContain(
|
||||
'image-canvas-editor__generation-submit',
|
||||
);
|
||||
expect(within(panel).getByRole('textbox', { name: 'prompt' })).toBeTruthy();
|
||||
expect(screen.getByLabelText('当前音效模型').textContent).toBe('audio1.0');
|
||||
const soundModelButton = within(panel).getByRole('button', {
|
||||
name: '音效模型 Vidu',
|
||||
});
|
||||
expect((soundModelButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(within(panel).getByText('Vidu')).toBeTruthy();
|
||||
expect(within(panel).queryByText(/Suno/u)).toBeNull();
|
||||
expect(
|
||||
@@ -570,6 +587,23 @@ describe('ImageCanvasGenerationComposerView', () => {
|
||||
expect(
|
||||
within(panel).getByRole('textbox', { name: 'gpt_description_prompt' }),
|
||||
).toBeTruthy();
|
||||
const footer = panel.querySelector(
|
||||
'.image-canvas-editor__generation-composer-footer',
|
||||
);
|
||||
expect(footer).toBeTruthy();
|
||||
expect(footer?.children[0]?.className).toContain(
|
||||
'image-canvas-editor__option-popover-anchor--model',
|
||||
);
|
||||
expect(footer?.children[1]?.className).toContain(
|
||||
'image-canvas-editor__generation-submit',
|
||||
);
|
||||
const backgroundMusicModelButton = within(panel).getByRole('button', {
|
||||
name: '背景音乐模型 Suno',
|
||||
});
|
||||
expect((backgroundMusicModelButton as HTMLButtonElement).disabled).toBe(
|
||||
true,
|
||||
);
|
||||
expect(within(panel).getByText('Suno')).toBeTruthy();
|
||||
expect(within(panel).queryByText('make_instrumental')).toBeNull();
|
||||
expect(within(panel).getByRole('button', { name: '生成游戏背景音乐' }).textContent).toBe(
|
||||
'生成5泥点',
|
||||
|
||||
@@ -670,6 +670,10 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
const soundDuration = normalizeSoundDuration(dialog.soundDurationSeconds);
|
||||
const soundOptionLabel = `${soundDuration}秒`;
|
||||
const dialogLabel = isSoundEffect ? '生成游戏音效' : '生成游戏背景音乐';
|
||||
const fixedModelLabel = isSoundEffect ? currentSoundModel.label : 'Suno';
|
||||
const fixedModelAriaLabel = isSoundEffect
|
||||
? `音效模型 ${fixedModelLabel}`
|
||||
: `背景音乐模型 ${fixedModelLabel}`;
|
||||
const cost = isSoundEffect
|
||||
? calculateEditorSoundEffectPrice(soundModel)
|
||||
: calculateEditorBackgroundMusicPrice(DEFAULT_BACKGROUND_MUSIC_MODEL);
|
||||
@@ -737,65 +741,65 @@ function ImageCanvasAudioGenerationComposerView({
|
||||
) : null}
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
{isSoundEffect ? (
|
||||
<>
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
||||
<PlatformInlineOptionButton
|
||||
ref={soundModelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
aria-label={`音效模型 ${currentSoundModel.label}`}
|
||||
ref={soundOptionsButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
|
||||
aria-label={`音效时长 ${soundOptionLabel}`}
|
||||
aria-expanded={isSoundOptionsOpen}
|
||||
disabled={isGenerating}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => setIsSoundOptionsOpen((isOpen) => !isOpen)}
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Music />
|
||||
</span>
|
||||
<span>{currentSoundModel.label}</span>
|
||||
</span>
|
||||
{soundOptionLabel}
|
||||
</PlatformInlineOptionButton>
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions">
|
||||
<PlatformInlineOptionButton
|
||||
ref={soundOptionsButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
|
||||
aria-label={`音效时长 ${soundOptionLabel}`}
|
||||
aria-expanded={isSoundOptionsOpen}
|
||||
disabled={isGenerating}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
onClick={() => setIsSoundOptionsOpen((isOpen) => !isOpen)}
|
||||
>
|
||||
{soundOptionLabel}
|
||||
</PlatformInlineOptionButton>
|
||||
{isSoundOptionsOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--audio-options image-canvas-editor__portal-menu"
|
||||
label="音效时长选项"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
soundOptionsButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__audio-type-options">
|
||||
{SOUND_EFFECT_DURATION_OPTIONS.map((option) => (
|
||||
<VideoOptionChoice
|
||||
key={option}
|
||||
selected={soundDuration === option}
|
||||
disabled={isGenerating}
|
||||
ariaLabel={`时长 ${option}秒`}
|
||||
onClick={() => updateSoundDuration(option)}
|
||||
>
|
||||
{option}秒
|
||||
</VideoOptionChoice>
|
||||
))}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
</>
|
||||
{isSoundOptionsOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--audio-options image-canvas-editor__portal-menu"
|
||||
label="音效时长选项"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
soundOptionsButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__audio-type-options">
|
||||
{SOUND_EFFECT_DURATION_OPTIONS.map((option) => (
|
||||
<VideoOptionChoice
|
||||
key={option}
|
||||
selected={soundDuration === option}
|
||||
disabled={isGenerating}
|
||||
ariaLabel={`时长 ${option}秒`}
|
||||
onClick={() => updateSoundDuration(option)}
|
||||
>
|
||||
{option}秒
|
||||
</VideoOptionChoice>
|
||||
))}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__readonly-generation-option">
|
||||
<PlatformInlineOptionButton
|
||||
ref={soundModelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
aria-label={fixedModelAriaLabel}
|
||||
disabled
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Music />
|
||||
</span>
|
||||
<span>{fixedModelLabel}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
|
||||
Reference in New Issue
Block a user