diff --git a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md index 8cca4462b..b90258ce3 100644 --- a/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md +++ b/docs/【编辑器】画板音乐生成入口设计-2026-06-18.md @@ -256,6 +256,7 @@ SFX V2 已完成产品与技术口径冻结及 T0–T6 工程实施;这不表 - 点击 AI 补全时先按统一规则规范化输入框首尾空白并同步写回;至少 2 个有效字符且总字符数不超过 200 时,前端才把这份可见最终 Prompt 传给登录态内部 BFF。 - Prompt 助手当前使用专用请求模型 `gpt-5.6-luna`,请求级 `reasoning_effort` 固定为 `medium`;画布 Agent 本身仍使用现有编辑器 Agent LLM 配置中的 `gpt-5.4-mini`。两者复用现有 `LlmClient`,不建立新的平台 LLM 能力。 +- AI 补全固定 completion tokens 总预算为 `2048`;当前 VectorEngine OpenAI Chat wire 发送 `max_completion_tokens = 2048`。该预算包含模型可能消耗的隐藏 reasoning tokens 与可见输出 tokens,不含输入 Prompt tokens,也不保证可见正文长度。 - 服务端模板必须要求:保留用户明确的主题、场景、风格、情绪、乐器、能量、韵律、时长、循环和避免项;按场景选择性补足场景、氛围、能量、韵律、乐器、旋律、声音设计、循环和避免项,不为凑全方向堆砌形容词。 - 用户描述已足够完整时,只补充一至两个与主题匹配的具体声音细节。发现冲突时,优先级为“明确避免项和限制 > 明确玩法用途与场景 > 风格、情绪、能量与韵律 > AI 补充细节”。 - 内部 envelope 的 `prompt` 字段只允许包含一条可直接写回输入框的中文 BGM Prompt;候选文本本身不得包含解释、标题、Markdown、JSON、代码块、具体艺人或歌曲模仿要求。 @@ -270,6 +271,7 @@ SFX V2 已完成产品与技术口径冻结及 T0–T6 工程实施;这不表 ### 一键简化 - 点击一键简化时先按统一规则规范化输入框首尾空白并同步写回;只在规范化后的当前 Prompt 至少含 1 个有效字符且总字符数为 201–2000 时允许调用。超过 2000 时完整保留文本,但不得调用简化 BFF。点击前保存这份规范化后的完整 Prompt,AI 处理中保持输入框不变。 +- 一键简化的每个业务语义轮(包括 180 字首轮和 170 字第二轮)固定 completion tokens 总预算为 `8192`;当前 VectorEngine OpenAI Chat wire 发送 `max_completion_tokens = 8192`。该预算包含模型可能消耗的隐藏 reasoning tokens 与可见输出 tokens,不含输入 Prompt tokens,也不保证可见正文长度。 - 服务端在本次简化中冻结 `originalPrompt` 为入站 canonical Prompt,最多两个业务语义轮期间始终不变,作为内容保真参照。第一次业务语义轮使用 `currentPrompt = originalPrompt`,目标为 180 字。这里的 `originalPrompt` / `currentPrompt` 是服务端组装 LLM 简化模板时的内部变量;客户端简化 BFF DTO 仍只提交一个 `currentPrompt`,该入站值经 canonicalization 后同时成为内部 `originalPrompt` 和第一次内部 `currentPrompt`。每个业务语义轮内部由 `LlmClient` 按现有配置执行的 transport retry 不增加业务语义轮数。 - 180 不是硬门槛。简化使用与补全相同的四字段内部结构化 envelope;envelope 不属于候选文本,也不得写回输入框或通过 BFF 暴露。响应不能解析为包含上述正确字段类型的对象时,本次候选不通过。 - 候选“格式合法”专指 `isDirectWritebackFormat = true`:模型确认 canonical 候选只包含一条可直接写回输入框的中文 BGM Prompt,不包含解释、标题、Markdown、JSON、代码块、字数报告、处理过程或删改说明。它与“内部结构可解析”是两个独立校验项;程序不得另用关键词或未定义的正则推断标题、解释等语义格式。 diff --git a/server-rs/crates/api-server/src/editor_background_music_prompt_assist.rs b/server-rs/crates/api-server/src/editor_background_music_prompt_assist.rs index af3b0906f..60483181e 100644 --- a/server-rs/crates/api-server/src/editor_background_music_prompt_assist.rs +++ b/server-rs/crates/api-server/src/editor_background_music_prompt_assist.rs @@ -26,7 +26,8 @@ use crate::{ state::AppState, }; -const BACKGROUND_MUSIC_PROMPT_ASSIST_MAX_OUTPUT_TOKENS: u32 = 1024; +const BACKGROUND_MUSIC_PROMPT_COMPLETION_MAX_OUTPUT_TOKENS: u32 = 2048; +const BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_OUTPUT_TOKENS: u32 = 8192; const BACKGROUND_MUSIC_PROMPT_ASSIST_MODEL: &str = "gpt-5.6-luna"; const BACKGROUND_MUSIC_PROMPT_MAX_CHARS: usize = platform_audio::SUNO_GPT_DESCRIPTION_PROMPT_MAX_CHARS; @@ -153,6 +154,7 @@ async fn run_background_music_prompt_completion( .run(build_background_music_prompt_llm_request( background_music_completion_system_prompt(), background_music_completion_user_prompt(current_prompt), + BACKGROUND_MUSIC_PROMPT_COMPLETION_MAX_OUTPUT_TOKENS, )) .await .map_err(map_background_music_prompt_llm_error)?; @@ -173,6 +175,7 @@ async fn run_background_music_prompt_simplification( original_prompt, BACKGROUND_MUSIC_SIMPLIFICATION_FIRST_TARGET_CHARS, ), + BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_OUTPUT_TOKENS, )) .await .map_err(map_background_music_prompt_llm_error)?; @@ -196,6 +199,7 @@ async fn run_background_music_prompt_simplification( second_current_prompt, BACKGROUND_MUSIC_SIMPLIFICATION_SECOND_TARGET_CHARS, ), + BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_OUTPUT_TOKENS, )) .await .map_err(map_background_music_prompt_llm_error)?; @@ -208,6 +212,7 @@ async fn run_background_music_prompt_simplification( fn build_background_music_prompt_llm_request( system_prompt: String, user_prompt: String, + max_output_tokens: u32, ) -> LlmRunRequest { LlmRunRequest::new(vec![ LlmMessage::system(system_prompt), @@ -216,7 +221,7 @@ fn build_background_music_prompt_llm_request( .with_model(BACKGROUND_MUSIC_PROMPT_ASSIST_MODEL) .with_openai_chat() .with_response_reasoning_effort(LlmResponseReasoningEffort::Medium) - .with_max_output_tokens(BACKGROUND_MUSIC_PROMPT_ASSIST_MAX_OUTPUT_TOKENS) + .with_max_output_tokens(max_output_tokens) } fn inspect_background_music_prompt_candidate( @@ -467,8 +472,11 @@ mod tests { #[test] fn llm_request_uses_editor_gpt_chat_without_tools() { - let request = - build_background_music_prompt_llm_request("系统".to_string(), "用户".to_string()); + let request = build_background_music_prompt_llm_request( + "系统".to_string(), + "用户".to_string(), + BACKGROUND_MUSIC_PROMPT_COMPLETION_MAX_OUTPUT_TOKENS, + ); assert_eq!(request.model.as_deref(), Some("gpt-5.6-luna")); assert_eq!(request.api_kind, LlmApiKind::OpenAiChat); @@ -478,7 +486,7 @@ mod tests { ); assert_eq!( request.max_output_tokens, - Some(BACKGROUND_MUSIC_PROMPT_ASSIST_MAX_OUTPUT_TOKENS) + Some(BACKGROUND_MUSIC_PROMPT_COMPLETION_MAX_OUTPUT_TOKENS) ); assert!(request.function_tools.is_empty()); assert_eq!(request.tool_choice, None); @@ -889,7 +897,7 @@ mod tests { ); assert_eq!( requests[0].body["max_completion_tokens"], - BACKGROUND_MUSIC_PROMPT_ASSIST_MAX_OUTPUT_TOKENS + BACKGROUND_MUSIC_PROMPT_COMPLETION_MAX_OUTPUT_TOKENS ); assert!(requests[0].body.get("max_tokens").is_none()); assert_eq!(requests[0].body["reasoning_effort"], "medium"); @@ -1033,6 +1041,12 @@ mod tests { let requests = mock.finish(); assert_eq!(requests.len(), 2); + for request in &requests { + assert_eq!( + request.body["max_completion_tokens"], + BACKGROUND_MUSIC_PROMPT_SIMPLIFICATION_MAX_OUTPUT_TOKENS + ); + } let first_input = user_message_json(&requests[0].body); assert_eq!(first_input["originalPrompt"], original_prompt); assert_eq!(first_input["currentPrompt"], original_prompt);