let currentStylePreset = null;
let currentStyleParams = {};

// 加载风格预设列表
async function loadStylePresets() {
    try {
        const r = await fetch(`${API_BASE}/style-presets`);
        const d = await r.json();
        if (d.success && d.presets) {
            allPresets = d.presets;
            const sel = document.getElementById('cfgStylePreset');
            if (sel) {
                sel.innerHTML = '<option value="">自动推荐</option>';
                for (const [pid, p] of Object.entries(d.presets)) {
                    sel.innerHTML += `<option value="${pid}">${p.icon} ${p.name}</option>`;
                }
            }
        }
    } catch(e) { console.warn('加载风格预设失败:', e); }
}

// 选择风格预设后渲染参数滑块
async function onStylePresetChange(presetId) {
    const desc = document.getElementById('stylePresetDesc');
    const sliders = document.getElementById('styleParamsSliders');

    if (!presetId) {
        if (desc) desc.textContent = '将根据描述自动推荐风格';
        if (sliders) sliders.innerHTML = '
选择风格后显示参数
';
        currentStylePreset = null;
        currentStyleParams = {};
        return;
    }

    try {
        const r = await fetch(`${API_BASE}/style-presets/${presetId}`);
        const d = await r.json();
        if (!d.success) return;

        const preset = d.preset;
        currentStylePreset = presetId;
        currentStyleParams = {};

        if (desc) desc.textContent = preset.description;

        // v3.20: 分组渲染 - 图片参数 / 负面提示词 / 视频参数
        let htmlImg = '';
        let htmlNeg = '';
        let htmlVideo = '';
        let negDatalistId = 0;

        for (const [pname, pdef] of Object.entries(preset.params)) {
            currentStyleParams[pname] = pdef.default;
            const isNeg = pname.startsWith('negative_prompt_');
            const isVideo = ['slow_motion_ratio','camera_move_speed','shot_duration',
                             'dynamic_amplitude','camera_type','transition_style','frame_rate'].includes(pname);

            if (isNeg) {
                // v3.20: 负面提示词 - textarea, 和提示词放在一起
                const icon = pdef.img_type === 'character' ? '👤' : pdef.img_type === 'scene' ? '🏞' : '🎬';
                htmlNeg += `

${icon} ${pdef.label}

                    <textarea class="form-control form-control-sm" id="sp_${pname}" rows="2"
                        oninput="onStyleParamChange('${pname}', this.value)"
                        style="font-size:0.85em;resize:vertical">${pdef.default}</textarea>

${pdef.desc}

`;
            } else if (pdef.allow_custom && pdef.options) {
                // v3.20: 可选+可输入 (运镜/转场) - input + datalist
                const dlId = `dl_${pname}_${++negDatalistId}`;
                let opts = pdef.options.map(o => `<option value="${o}">`).join('');
                htmlVideo += `

${pdef.label} (可选项或自定义)

                    <input type="text" class="form-control form-control-sm" id="sp_${pname}"
                        list="${dlId}" value="${pdef.default}"
                        oninput="onStyleParamChange('${pname}', this.value)" style="font-size:0.9em">
                    <datalist id="${dlId}">${opts}</datalist>

${pdef.desc}

`;
            } else if (pdef.min !== null && pdef.min !== undefined) {
                // 数值滑块
                const target = isVideo ? htmlVideo : htmlImg;
                const block = `

                        ${pdef.label}
                        ${pdef.default}

                    <input type="range" class="form-range" style="height:6px"
                        id="sp_${pname}"
                        min="${pdef.min}" max="${pdef.max}" step="${pdef.step}" value="${pdef.default}"
                        oninput="onStyleParamChange('${pname}', this.value)">

                        ${pdef.min}${pdef.max}

${pdef.desc}

`;
                if (isVideo) htmlVideo += block; else htmlImg += block;
            } else if (pdef.options) {
                // 选项下拉(不允许自定义)
                const block = `

${pdef.label}

                    <select class="form-select form-select-sm" id="sp_${pname}"
                        onchange="onStyleParamChange('${pname}', this.value)" style="font-size:0.9em">
                        ${pdef.options.map(o => `<option value="${o}" ${o === pdef.default ? 'selected' : ''}>${o}</option>`).join('')}
                    </select>

${pdef.desc}

`;
                if (isVideo) htmlVideo += block; else htmlImg += block;
            } else {
                // 普通文本
                const block = `

${pdef.label}

                    <input type="text" class="form-control form-control-sm" id="sp_${pname}"
                        value="${pdef.default}"
                        oninput="onStyleParamChange('${pname}', this.value)" style="font-size:0.9em">

${pdef.desc}

`;
                if (isVideo) htmlVideo += block; else htmlImg += block;
            }
        }

        // 组装: 图片参数 + 负面提示词 + 视频参数
        let html = '';
        if (htmlImg) html += `<div class="mb-2"><div style="font-weight:600;border-bottom:1px solid #dee2e6;padding-bottom:2px;margin-bottom:4px">🖼 图片参数</div>${htmlImg}</div>`;
        if (htmlNeg) html += `<div class="mb-2"><div style="font-weight:600;border-bottom:1px solid #dee2e6;padding-bottom:2px;margin-bottom:4px">🚫 负面提示词</div>${htmlNeg}</div>`;
        if (htmlVideo) html += `<div class="mb-2"><div style="font-weight:600;border-bottom:1px solid #dee2e6;padding-bottom:2px;margin-bottom:4px">🎬 视频参数</div>${htmlVideo}</div>`;

        if (sliders) sliders.innerHTML = html;

    } catch(e) { console.warn('加载风格预设详情失败:', e); }
}

// 参数值变更
function onStyleParamChange(paramName, value) {
    // 数值参数转float
    const el = document.getElementById(`sp_${paramName}`);
    if (el && el.type === 'range') {
        value = parseFloat(value);
        const valEl = document.getElementById(`sp_val_${paramName}`);
        if (valEl) valEl.textContent = value;
    }
    currentStyleParams[paramName] = value;
}

// 获取当前风格配置(用于任务创建和配置保存)
function getStyleConfig() {
    return {
        style_preset: currentStylePreset || null,
        style_params: Object.keys(currentStyleParams).length > 0 ? currentStyleParams : null
    };
}

// 初始化风格预设
document.addEventListener('DOMContentLoaded', () => {
    loadStylePresets();
    const sel = document.getElementById('cfgStylePreset');
    if (sel) sel.addEventListener('change', (e) => onStylePresetChange(e.target.value));
});


现在我来检查一下写步骤是如何渲染的（以了解提示词的显示位置），以及任务系统如何处理镜头级别的视频参数：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "camera_type\|transition_style\|_get_style_params\|style_img_params\|style_video_params\|image_character\|image_scene\|image_shot\|shot_config\|镜头.*参数\|运镜\|转场" user_driven_task_system.py | head -40


file lock stale for /root/.openclaw/agents/cto/sessions/feb329d1-ab6a-4af7-89ea-70c17d09ac4a.jsonl


650:    def _get_style_params(self, task_data: dict, param_type: str = "image") -> dict:
656:                'image_character' 角色图参数(含角色负面提示词)
657:                'image_scene' 场景图参数(含场景负面提示词)
658:                'image_shot' 镜头参考图参数(含镜头负面提示词)
700:        if param_type == "image_scene":
702:        elif param_type == "image_shot":
721:                       "dynamic_amplitude", "camera_type", "transition_style", "frame_rate"]
771:        style_img_params = self._get_style_params(task_data or {}, "image_character") if task_data else {
807:                        **style_img_params,  # v3.19: 风格预设驱动参数
826:                            **style_img_params,  # v3.19: 风格预设驱动参数
839:                            **style_img_params,  # v3.19: 风格预设驱动参数
910:        style_img_params = self._get_style_params(task_data or {}, "image_scene") if task_data else {
1022:                        **style_img_params,  # v3.19: 风格预设驱动参数
1052:                            **style_img_params,  # v3.19: 风格预设驱动参数
1128:        style_img_params = self._get_style_params(task_data or {}, "image_shot") if task_data else {
1209:                    **style_img_params,  # v3.19: 风格预设驱动参数
1988:        style_video_params = self._get_style_params(task_data, "video")
2060:                                    **style_video_params,
2751:                                    **style_video_params,
2760:                                    **style_video_params,
3268:                        **style_video_params,


    def _get_style_params(self, task_data: dict, param_type: str = "image") -> dict:
        """v3.20: 从任务的风格预设中获取生成参数

        Args:
            task_data: 任务数据
            param_type:
                'image_character' 角色图参数(含角色负面提示词)
                'image_scene' 场景图参数(含场景负面提示词)
                'image_shot' 镜头参考图参数(含镜头负面提示词)
                'image' 兼容旧调用(返回角色图参数)
                'video' 视频参数
                'all' 全部

        Returns:
            dict: 可直接传给generate_image/generate_video的关键字参数
                  negative_prompt已根据图片类型映射为对应值
        """
        from style_presets import get_preset, recommend_preset, validate_and_clamp

        preset_id = task_data.get("style_preset")
        user_params = task_data.get("style_params", {})

        # 如果没有预设，根据描述自动推荐
        if not preset_id:
            description = task_data.get("description", "")
            visual_style = task_data.get("results", {}).get("outline", {}).get("visual_style_recommendation")
            rec = recommend_preset(description, visual_style)
            preset_id = rec["preset_id"]
            logger.info(f"  🎨 自动推荐风格: {rec['preset_name']} ({rec['reason'][:60]})")

        # 获取预设定义
        preset = get_preset(preset_id)
        if not preset:
            logger.warning(f"  未知风格预设 {preset_id}, 使用默认参数")
            return {}

        # 合并用户参数并钳制
        if user_params:
            validated = validate_and_clamp(preset_id, user_params)
            if validated.get("_warnings"):
                for w in validated["_warnings"]:
                    logger.warning(f"  ⚠ 风格参数调整: {w}")
                del validated["_warnings"]
            final_params = validated
        else:
            final_params = {k: v["default"] for k, v in preset["params"].items()}

        # v3.20: 根据图片类型选择对应的negative_prompt
        # negative_prompt_character/scene/shot → 统一映射为 negative_prompt
        img_type = "character"  # 默认
        if param_type == "image_scene":
            img_type = "scene"
        elif param_type == "image_shot":
            img_type = "shot"
        elif param_type == "image":
            img_type = "character"  # 兼容旧调用

        # 将类型化负面提示词映射为API所需的negative_prompt
        neg_key = f"negative_prompt_{img_type}"
        if neg_key in final_params:
            final_params["negative_prompt"] = final_params[neg_key]

        logger.info(f"  🎨 风格预设: {preset['name']} | 类型: {param_type} | 负面词: {img_type}")

        # 按类型过滤
        image_base_params = ["character_consistency", "style_strength", "image_quality",
                       "scene_detail_level", "color_tone", "light_effect",
                       "composition_frame", "negative_prompt"]
        # 排除类型化的负面提示词(已映射为统一的negative_prompt)
        exclude_keys = {"negative_prompt_character", "negative_prompt_scene", "negative_prompt_shot"}
        video_params = ["slow_motion_ratio", "camera_move_speed", "shot_duration",
                       "dynamic_amplitude", "camera_type", "transition_style", "frame_rate"]

        if param_type.startswith("image"):
            return {k: v for k, v in final_params.items() if k in image_base_params and k not in exclude_keys}
        elif param_type == "video":
            return {k: v for k, v in final_params.items() if k in video_params}
        else:
            return {k: v for k, v in final_params.items() if k not in exclude_keys}

[3295 more lines in file. Use offset=730 to continue.]


现在我来检查一下写步骤的渲染，以了解提示词的显示位置：


if(m.includes('SensitiveContent')||m.includes('sensitive'))return{reason:'参考图被检测为敏感内容',suggestion:'建议：1)点击"改为文生视频"重新生成 2)修改参考图提示词'};
    if(m.includes('timeout')||m.includes('超时'))return{reason:'视频生成超时',suggestion:'建议：稍后重试，或使用文生视频模式'};
    if(m.includes('400')||m.includes('BadRequest'))return{reason:'请求参数有误',suggestion:'建议：简化提示词后重试'};
    if(m.includes('429')||m.includes('rate'))return{reason:'API频率超限',suggestion:'建议：等待几分钟后重试'};
    return{reason:m.substring(0,100),suggestion:'建议：尝试重新生成，或切换到文生视频模式'};
}

// Display functions
function displayCollectionResult(r,autoExpanded){const c=document.getElementById('collectResultContent');if(!r){c.innerHTML='
暂无
';return;}let h='';if(r.agent_called)h+='
🤖 由采集官Agent执行
';else if(r.agent_called===false)h+='
📝 由LLM直接执行
';if(r.from_db)h+='
📂 来自素材库
';if(r.analysis)h+=`<div class="alert alert-info"><h6>📡 采集分析</h6><pre style="white-space:pre-wrap;margin:0;font-size:.9em">${escapeHtml(r.analysis)}</pre></div>`;c.innerHTML=h||'
暂无
';if(!autoExpanded)expandStep('collect');}

function displayWriteResult(result,autoExpanded){
    const c=document.getElementById('writeResultContent');if(!result){c.innerHTML='
暂无
';return;}
    window._wr=result;let h='';if(result.agent_called)h+='
🤖 由创作官Agent执行
';else if(result.agent_called===false)h+='
📝 由LLM直接执行
';
    if(result.title)h+=`<h5 class="mb-2">📖 ${escapeHtml(result.title)}</h5>`;
    let ci=0,si=0,sri=0;
    if(result.characters)ci=result.characters.filter(x=>x.image_path).length;
    if(result.scenes)result.scenes.forEach(s=>{if(s.image_path)si++;if(s.shots)sri+=s.shots.filter(x=>x.image_path).length;});
    if(ci||si||sri){h+=`<div class="alert alert-success py-2 mb-3">🎨 角色图=${ci} | 🏞 场景图=${si} | 📸 镜头参考图=${sri}</div>`;}
    // 确认草稿按钮：场景图未全部生成时显示
    let totalScenes=result.scenes?result.scenes.length:0;
    let needConfirm=totalScenes>0&&si<totalScenes;
    if(needConfirm){h+=`
<button class="btn btn-success" onclick="confirmDraft()">确认草稿并生成场景图</button> <button class="btn btn-outline-primary" onclick="batchGenerateSceneImages()">批量生成场景图</button>
`;}
    if(result.characters&&result.characters.length){h+='
👥 角色

<button class="btn btn-outline-warning btn-sm" onclick="batchRegenerateCharImages()">重新生成全部角色图</button>

';result.characters.forEach((ch,i)=>{
        let desc=ch.appearance||ch.description||ch.personality||'';
        let threeView=ch.three_view_prompt||'';
        h+=`

${ch.image_path?lazyImg(getImageUrl(ch.image_path),'250px'):'
暂无图片
'}${escapeHtml(ch.name)}
${escapeHtml(desc)}
${threeView?`<details class="mb-1"><summary class="small text-info">📐 三视图提示词</summary><p class="small bg-light p-1 rounded mt-1">${escapeHtml(threeView)}</p></details>`:''}<button class="btn btn-sm btn-outline-warning mt-1" onclick="regenerateCharImage(${i})">修改提示词重生成</button>

`;});h+='
';}
    if(result.scenes&&result.scenes.length){let ts=0;result.scenes.forEach(s=>ts+=(s.shots?.length||0));let missingShots=0;result.scenes.forEach(s=>{if(s.shots)s.shots.forEach(sh=>{if(!sh.image_path)missingShots++;});});let missingScenes=result.scenes.filter(s=>!s.image_path).length;h+=`<h6>🎬 分镜 (${result.scenes.length}场景/${ts}镜头)</h6>`;h+=`<div class="mb-2 d-flex gap-2 flex-wrap">`;h+=`<button class="btn btn-outline-warning btn-sm" onclick="batchRegenerateSceneImages()"><i class="bi bi-arrow-clockwise me-1"></i>重新生成全部背景图</button>`;if(missingScenes>0)h+=`<button class="btn btn-outline-primary btn-sm" onclick="batchGenerateSceneImages()"><i class="bi bi-play-circle me-1"></i>继续生成背景图 (${missingScenes}个未完成)</button>`;h+=`<button class="btn btn-outline-warning btn-sm" onclick="batchGenerateShotRefImages(true)"><i class="bi bi-arrow-clockwise me-1"></i>重新生成全部参考图</button>`;if(missingShots>0)h+=`<button class="btn btn-outline-primary btn-sm" onclick="batchGenerateShotRefImages(false)"><i class="bi bi-play-circle me-1"></i>继续生成参考图 (${missingShots}个未完成)</button>`;h+=`</div>`;result.scenes.forEach((sc,idx)=>{let sceneBtnLabel=sc.image_path?'重生成场景图':'生成场景图';h+=`

🏞 ${escapeHtml(sc.location||sc.description?.substring(0,20)||'场景'+sc.scene_id)}
${sc.image_path?lazyImg(getImageUrl(sc.image_path),'60px','','style="display:inline-block;width:60px"'):'  '}<button class="btn btn-sm btn-outline-warning ms-1" onclick="regenerateSceneImage(${idx})">${sceneBtnLabel}</button>

`;if(sc.shots)sc.shots.forEach((sh,shIdx)=>{h+=`

${sh.image_path?lazyImg(getImageUrl(sh.image_path),'120px','','style="width:90px"'):'
无图
'}
镜头${sh.shot_id} ${sh.shot_type||''} ${sh.duration_seconds||5}s
${escapeHtml(sh.description||'')}
${sh.dialogue?`
💬 "${escapeHtml(sh.dialogue)}"
`:''}<button class="btn btn-sm btn-outline-warning mt-1" onclick="regenerateShotRefImage(${idx},${shIdx})">${sh.image_path?'重生成参考图':'生成参考图'}</button>

`;});h+='

';});}
    c.innerHTML=h||'
暂无
';if(!autoExpanded)expandStep('write');
}

function displayProduceResult(result,autoExpanded){
    const c=document.getElementById('produceResultContent');if(!result){c.innerHTML='
暂无
';return;}
    window._pr=result;let h='';const ok=result.succeeded||0,tot=result.total_shots||0;
    h+=`<div class="alert alert-success py-2 mb-3">✅ ${ok}/${tot}镜头成功 | 🎨图生=${result.img2vid_count||0} | ✏文生=${result.text2vid_count||0}</div>`;
    if(result.stitch_result){if(result.stitch_result.status==='succeeded'){const su=getVideoUrl(result.stitch_result.output_path);h+=`
🎞 已拼接: ${result.stitch_result.duration?.toFixed(1)||'?'}秒 <button class="btn btn-sm btn-outline-info ms-2" onclick="showVideoModal('${su}')">▶ 全屏播放</button>
`;
            h+=`<div class="mb-3">${lazyVideo(su)}</div>`;}else if(result.stitch_result.status==='failed'){h+=`<div class="alert alert-warning py-2 mb-2">🎞 拼接失败: ${escapeHtml(result.stitch_result.error||'未知原因')} <button class="btn btn-sm btn-outline-warning ms-2" onclick="stitchVideos()">🔄 重新拼接</button></div>`;}else{h+=`<div class="alert alert-secondary py-2 mb-2">🎞 未拼接 <button class="btn btn-sm btn-outline-info ms-2" onclick="stitchVideos()">🔗 开始拼接</button></div>`;}}
    document.getElementById('btnStitchVideos').style.display=(ok>=2)?'inline-block':'none';
    if(result.shots&&result.shots.length){h+='
🎬 各镜头

';result.shots.forEach(shot=>{const ml=shot.mode==='img2vid'?'🎨图生':shot.mode==='text2vid_fallback'?'⚠降级文生':'✏文生';const sl=shot.status==='succeeded'?'✅':shot.status==='failed'?'❌':'⏭';h+=`

镜头${shot.shot_id}${shot.has_subtitles?' 📝字幕':''}
${ml} ${sl}

`;if(shot.ref_image)h+=`
参考图:${lazyImg(getImageUrl(shot.ref_image),'80px')}
`;if(shot.local_path&&shot.status==='succeeded'){const vu=getVideoUrl(shot.local_path);h+=`
<button class="btn btn-sm btn-outline-primary" onclick="showVideoModal('${vu}')">▶ 播放</button> 💾 下载
`;
                    /* v3.11: 不再内联播放视频，用播放按钮代替 */}if(shot.error||shot.status==='failed'){const a=analyzeVideoError(shot.error);if(a)h+=`<div class="alert alert-danger py-1 mt-2 small"><strong>❌ 原因:</strong> ${escapeHtml(a.reason)}<br><strong>💡 建议:</strong> ${escapeHtml(a.suggestion)}</div>`;else h+=`<div class="mt-1 text-danger small">❌ ${escapeHtml(shot.error||'未知')}</div>`;}h+=`
<button class="btn btn-sm btn-outline-warning" onclick="doRegenShot(${shot.shot_id})">重新生成</button> <button class="btn btn-sm btn-outline-info" onclick="doRegenShotPrompt(${shot.shot_id})">修改提示词重生成</button>${shot.error&&shot.mode==='img2vid'?` <button class="btn btn-sm btn-outline-success" onclick="doRegenShotText(${shot.shot_id})"><i class="bi bi-type me-1"></i>改为文生视频</button>`:''}

`;});}
    c.innerHTML=h||'
暂无
';if(!autoExpanded)expandStep('produce');
}

function displayReviewResult(r,autoExpanded){const c=document.getElementById('reviewResultContent');if(!r){c.innerHTML='
暂无
';return;}let h='';if(r.agent_called)h+='
🤖 由审核官Agent执行
';else if(r.agent_called===false)h+='
📝 由LLM直接执行
';
if(r.quality_assessment){const qa=r.quality_assessment;h+='
📊 质量评估

';[{key:'overall_quality',label:'总体',color:'#0066cc'},{key:'creativity',label:'创意',color:'#9c27b0'},{key:'technical_execution',label:'技术',color:'#ff9800'},{key:'storytelling',label:'叙事',color:'#4caf50'},{key:'visual_impact',label:'视觉',color:'#e91e63'}].forEach(d=>{const v=qa[d.key];if(v!==undefined){const pct=Math.round(v*10);const bc=pct>=70?'border-color:'+d.color:pct>=50?'border-color:#ffc107':'border-color:#dc3545';h+=`<div class="gauge-container"><div class="gauge-ring" style="border:3px solid;${bc};">${pct}</div><small>${d.label}</small></div>`;}});h+='
';}
if(r.compliance_check){h+='
✅ 合规检查

';Object.entries(r.compliance_check).forEach(([k,v])=>{if(typeof v==='boolean')h+=`<span class="badge ${v?'bg-success':'bg-danger'} me-1 mb-1">${k}: ${v?'✓':'✗'}</span>`;});h+='
';}

⚠ [... middle content omitted — showing head and tail ...]

async function doRegenShotPrompt(id){
    // Find current shot's video_prompt from window._pr or window._wr
    let currentPrompt='';
    if(window._pr&&window._pr.shots){const s=window._pr.shots.find(x=>x.shot_id===id);if(s&&s.video_prompt)currentPrompt=s.video_prompt;}
    if(!currentPrompt&&window._wr&&window._wr.scenes){for(const sc of window._wr.scenes){if(sc.shots){const s=sc.shots.find(x=>x.shot_id===id);if(s&&s.video_prompt){currentPrompt=s.video_prompt;break;}}}}
    const p=await showPromptEditor('修改镜头'+id+'的视频提示词',currentPrompt||'');
    if(p!==null)doRegenShot(id,p||undefined);
}
function doRegenShotText(id){doRegenShot(id,undefined,'text2vid');}

// Stitch
async function stitchVideos(){if(!currentTaskId)return;const btn=document.getElementById('btnStitchVideos');btn.disabled=true;btn.innerHTML=' 拼接中...';addLog('info','开始拼接视频...');try{const ctrl=new AbortController();const tid=setTimeout(()=>ctrl.abort(),180000);const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/stitch`,{method:'POST',signal:ctrl.signal});clearTimeout(tid);const d=await r.json();if(d.success){addLog('success','拼接成功！'+d.message);await viewStepResult('produce');}else addLog('error','拼接失败: '+d.message);}catch(e){addLog('error',e.name==='AbortError'?'拼接超时，请稍后重试':'拼接失败: '+e.message);}finally{btn.disabled=false;btn.innerHTML='拼接视频';}}

// Step execution
async function executeStep(step,btnId,lt,ot){if(!currentTaskId){addLog('error','请先创建或选择任务');return;}const btn=document.getElementById(btnId);if(!btn)return;btn.disabled=true;btn.innerHTML=' '+lt+'...';updateStepStatus(step,'working');addLog('info','开始'+getStepName(step)+'...');try{let u=`${API_BASE}/tasks/${currentTaskId}/steps/${step}`;if(step==='produce'){const vm=currentTaskData?.video_mode||document.getElementById('taskVideoMode')?.value||'img2vid';u+='?video_mode='+vm;}const ctrl=new AbortController();const timer=setTimeout(()=>ctrl.abort(),600000);const r=await fetch(u,{method:'POST',headers:{'Content-Type':'application/json'},signal:ctrl.signal});clearTimeout(timer);if(!r.ok)throw new Error('HTTP '+r.status+': '+(await r.text()).substring(0,200));const d=await r.json();if(d.success){updateStepStatus(step,'completed',false,d.can_modify,d.can_regenerate);const nx={collect:'write',write:'produce',produce:'review'}[step];if(nx)updateStepStatus(nx,'pending',true);if(d.result){const fn={collect:displayCollectionResult,write:displayWriteResult,produce:displayProduceResult,review:displayReviewResult}[step];if(fn)fn(d.result);}await refreshTaskStatus();addLog('success',getStepName(step)+'完成！'+(d.message||''));}else throw new Error(d.message||'失败');}catch(e){updateStepStatus(step,'pending',true);if(e.name==='AbortError')addLog('error',getStepName(step)+'超时(10分钟)，服务可能仍在后台执行');else addLog('error',getStepName(step)+'失败: '+e.message);}finally{btn.disabled=false;btn.innerHTML=ot;}}
async function viewStepResult(step){if(!currentTaskId)return;try{const r=await fetch(`${API_BASE}/tasks/${currentTaskId}/steps/${step}`);if(r.ok){const d=await r.json();if(d.success&&d.result){
    // v3.17: 智能判断write步骤的展示方式
    if(step==='write'){
        const res=d.result;
        if(res.scenes&&res.scenes.length>0&&res.scenes[0]?.shots){
            displayWriteResult(res);
        }else if(res.title||res.synopsis||res.characters){
            displayWritePhase1Result(res);
        }else{
            displayWriteResult(res);

[449 more lines in file. Use offset=310 to continue.][... 18811 more characters truncated]


现在我已充分理解。让我再检查一下 `api_server.py` 文件，以了解与风格相关的 `endpoints` 和版本号：

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "version\|3\.20\|3\.19\|3\.18\|style.presets\|style-presets\|recommend\|validate" api_server.py | head -30


88:    version="3.20"
177:        "version": "3.20",
513:            style_preset=request.get("style_preset", None),  # v3.19: 风格预设
514:            style_params=request.get("style_params", None),  # v3.19: 用户调整的风格参数
531:            "style_preset": task_data.get("style_preset"),  # v3.19
532:            "style_params": task_data.get("style_params", {}),  # v3.19
1026:    # v3.20: list_tasks不支持user_id过滤，返回全部任务
1067:        "version": "3.20",
1075:# v3.19: 风格预设API
1077:@app.get("/api/style-presets")
1078:async def list_style_presets():
1080:    from style_presets import get_all_presets
1083:@app.get("/api/style-presets/{preset_id}")
1086:    from style_presets import get_preset
1092:@app.post("/api/style-presets/recommend")
1093:async def recommend_style_preset(request: Request):
1098:    from style_presets import recommend_preset
1099:    result = recommend_preset(description, visual_style)
1100:    return {"success": True, "recommendation": result}
1102:@app.post("/api/style-presets/{preset_id}/validate")
1103:async def validate_style_params(preset_id: str, request: Request):
1107:    from style_presets import validate_and_clamp
1108:    result = validate_and_clamp(preset_id, user_params)


Explanation: 已读取所有关键文件，开始实施4个需求

读取当前代码文件(style_presets.py, 前端HTML/JS, api_server.py, task_system.py)

需求1: 负面提示词显示在对应正面提示词下面(角色/场景/镜头)

需求2: 视频参数按镜头配置,生成镜头时给出建议运镜/转场

需求3: 风格预设和图片参数移到第二步,给出建议值

需求4: 扩展风格预设,匹配Agent建议

版本号更新到v3.21 + 自测验证

Explanation: 已读取所有关键文件，开始实施4个需求

需求1: 负面提示词显示在对应正面提示词下面(角色/场景/镜头)

需求2: 视频参数按镜头配置,生成镜头时给出建议运镜/转场

需求3: 风格预设和图片参数移到第二步,给出建议值

需求4: 扩展风格预设,匹配Agent建议

版本号更新到v3.21 + 自测验证


Explanation: 已读取所有关键文件，开始实施4个需求

需求1: 负面提示词显示在对应正面提示词下面(角色/场景/镜头)

需求2: 视频参数按镜头配置,生成镜头时给出建议运镜/转场

需求3: 风格预设和图片参数移到第二步,给出建议值

需求4: 扩展风格预设,匹配Agent建议

版本号更新到v3.21 + 自测验证


好的，现在我已经完全理解了代码结构。开始实施4个需求。改动涉及多个文件，我会用subagent并行处理以加快速度。

先分析4个需求的核心改动：

**需求1**: 负面提示词显示在对应正面提示词下面 → 前端JS `onStylePresetChange` 渲染逻辑重组：不再把所有负面词放在一个"🚫 负面提示词"分组，而是把角色负面词放在角色图参数后面，场景负面词放在场景图参数后面，镜头负面词放在镜头图参数后面

**需求2**: 视频参数按镜头配置 → 后端：`_get_style_params` 需要支持镜头级别参数覆盖；前端：每个镜头卡片增加运镜/转场选择

**需求3**: 风格预设和图片参数移到第二步 → 前端：从左侧任务面板移到步骤2的创作区域，在创作结果上方展示

**需求4**: 扩展风格预设 → `style_presets.py` 增加更多风格

这是一个大改动，让我开始逐步实施：

edit

"""风格预设系统 - v3.20
定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
创作官推荐风格，用户在区间内微调
v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分
"""

"""风格预设系统 - v3.21
定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
创作官推荐风格，用户在区间内微调
v3.21: 扩展到8种风格预设, 负面提示词按图片类型分组显示, 镜头级视频参数
v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分
"""

# 按风格叠加的额外负面词
_NEG_STYLE_EXTRA = {
    "ancient_fantasy": {
        "character": "现代建筑,现代服饰,电子设备",
        "scene": "现代建筑,现代设施,汽车,电线杆",
        "shot": "现代元素,快节奏动感画面",
    },
    "urban_romance": {
        "character": "",
        "scene": "破旧脏乱,荒废场景",
        "shot": "",
    },
    "suspense_thriller": {
        "character": "明亮欢快表情,微笑表情",
        "scene": "明亮场景,欢快氛围,阳光明媚",
        "shot": "明亮欢快,温馨氛围",
    },
    "pastoral_healing": {
        "character": "阴暗表情,恐怖面容",
        "scene": "阴暗场景,恐怖元素,工业污染",
        "shot": "阴暗恐怖,紧张氛围",
    },
}

# 按风格叠加的额外负面词
_NEG_STYLE_EXTRA = {
    "ancient_fantasy": {
        "character": "现代建筑,现代服饰,电子设备",
        "scene": "现代建筑,现代设施,汽车,电线杆",
        "shot": "现代元素,快节奏动感画面",
    },
    "urban_romance": {
        "character": "",
        "scene": "破旧脏乱,荒废场景",
        "shot": "",
    },
    "suspense_thriller": {
        "character": "明亮欢快表情,微笑表情",
        "scene": "明亮场景,欢快氛围,阳光明媚",
        "shot": "明亮欢快,温馨氛围",
    },
    "pastoral_healing": {
        "character": "阴暗表情,恐怖面容",
        "scene": "阴暗场景,恐怖元素,工业污染",
        "shot": "阴暗恐怖,紧张氛围",
    },
    "youth_campus": {
        "character": "老年面容,严肃表情,职业装",
        "scene": "阴暗场景,破旧建筑,工业设施",
        "shot": "阴暗压抑,紧张氛围",
    },
    "sci_fi_future": {
        "character": "古代服饰,传统建筑,自然风光",
        "scene": "田园风光,古代建筑,自然景观",
        "shot": "慢节奏,静态画面,柔和光线",
    },
    "war_epic": {
        "character": "柔弱表情,轻松氛围,现代便装",
        "scene": "宁静田园,温馨家居,现代都市",
        "shot": "轻松欢快,慢节奏,柔和光线",
    },
    "comedy_farce": {
        "character": "严肃表情,恐怖面容,悲伤表情",
        "scene": "恐怖场景,阴暗环境,破败废墟",
        "shot": "紧张压抑,阴暗恐怖,慢节奏",
    },
}


Could not find edits[0] in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/style_presets.py. The oldText must match exactly including all whitespace and newlines.


"""
风格预设系统 - v3.20
定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
创作官推荐风格，用户在区间内微调
v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分

[548 more lines in file. Use offset=6 to continue.]


"""
风格预设系统 - v3.20
定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
创作官推荐风格，用户在区间内微调
v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分

"""
风格预设系统 - v3.21
定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
创作官推荐风格，用户在区间内微调
v3.21: 扩展到8种风格预设, 负面提示词按图片类型分组显示, 镜头级视频参数
v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分


Successfully replaced 1 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/style_presets.py.

   1 """
-  2 风格预设系统 - v3.20
+  2 风格预设系统 - v3.21
   3 定义慢剧风格预设，每个参数包含推荐值、允许区间、影响说明
   4 创作官推荐风格，用户在区间内微调
+  5 v3.21: 扩展到8种风格预设, 负面提示词按图片类型分组显示, 镜头级视频参数
   5 v3.20: 运镜/转场改为可选项+自定义输入, 负面提示词按角色/场景/镜头拆分
   6 """
   7
   8 # ============================================================
     ...


现在扩展 `_NEG_STYLE_EXTRA` 以包含4种新风格：

    "pastoral_healing": {
        "character": "阴暗表情,恐怖面容",
        "scene": "阴暗场景,恐怖元素,工业污染",
        "shot": "阴暗恐怖,紧张氛围",
    },
}

    "pastoral_healing": {
        "character": "阴暗表情,恐怖面容",
        "scene": "阴暗场景,恐怖元素,工业污染",
        "shot": "阴暗恐怖,紧张氛围",
    },
    "youth_campus": {
        "character": "老年面容,严肃表情,职业装",
        "scene": "阴暗场景,破旧建筑,工业设施",
        "shot": "阴暗压抑,紧张氛围",
    },
    "sci_fi_future": {
        "character": "古代服饰,传统建筑,自然风光",
        "scene": "田园风光,古代建筑,自然景观",
        "shot": "慢节奏,静态画面,柔和光线",
    },
    "war_epic": {
        "character": "柔弱表情,轻松氛围,现代便装",
        "scene": "宁静田园,温馨家居,现代都市",
        "shot": "轻松欢快,慢节奏,柔和光线",
    },
    "comedy_farce": {
        "character": "严肃表情,恐怖面容,悲伤表情",
        "scene": "恐怖场景,阴暗环境,破败废墟",
        "shot": "紧张压抑,阴暗恐怖,慢节奏",
    },
}


     ...
  58         "character": "阴暗表情,恐怖面容",
  59         "scene": "阴暗场景,恐怖元素,工业污染",
  60         "shot": "阴暗恐怖,紧张氛围",
  61     },
+ 62     "youth_campus": {
+ 63         "character": "老年面容,严肃表情,职业装",
+ 64         "scene": "阴暗场景,破旧建筑,工业设施",
+ 65         "shot": "阴暗压抑,紧张氛围",
+ 66     },
+ 67     "sci_fi_future": {
+ 68         "character": "古代服饰,传统建筑,自然风光",
+ 69         "scene": "田园风光,古代建筑,自然景观",
+ 70         "shot": "慢节奏,静态画面,柔和光线",
+ 71     },
+ 72     "war_epic": {
+ 73         "character": "柔弱表情,轻松氛围,现代便装",
+ 74         "scene": "宁静田园,温馨家居,现代都市",
+ 75         "shot": "轻松欢快,慢节奏,柔和光线",
+ 76     },
+ 77     "comedy_farce": {
+ 78         "character": "严肃表情,恐怖面容,悲伤表情",
+ 79         "scene": "恐怖场景,阴暗环境,破败废墟",
+ 80         "shot": "紧张压抑,阴暗恐怖,慢节奏",
+ 81     },
  62 }
  63
  64 def _build_neg(style_id: str, img_type: str) -> str:
  65     """构建负面提示词: 基础词 + 类型词 + 风格词"""
     ...


现在，在 `pastoral_healing` 之后添加 4 个新的风格预设。我将读取文件末尾以找到合适的位置：


            "negative_prompt_shot": {
                "default": _build_neg("pastoral_healing", "shot"),
                "label": "镜头负面提示词",
                "desc": "镜头参考图排除的元素。治愈镜头需排除紧张氛围",
                "img_type": "shot",
            },
            "slow_motion_ratio": {
                "default": 2.0, "min": 1.5, "max": 3.0, "step": 0.5,
                "label": "慢放倍率",
                "desc": "慢放程度。治愈剧节奏舒缓，慢放增加沉浸感",
            },
            "dynamic_amplitude": {
                "default": 0.05, "min": 0.0, "max": 0.1, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。治愈剧画面应稳定安宁",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "固定机位", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。固定机位最安宁。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "淡入淡出", "min": None, "max": None, "step": None,

[135 more lines in file. Use offset=440 to continue.]


                "label": "镜头转场",
                "desc": "镜头间转场方式。淡入淡出最柔和。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },
}

# ============================================================
# 风格关键词 → 预设映射 (创作官推荐用)
# ============================================================
STYLE_KEYWORD_MAP = {
    # 古风仙侠
    "古风": "ancient_fantasy", "仙侠": "ancient_fantasy", "修仙": "ancient_fantasy",
    "武侠": "ancient_fantasy", "玄幻": "ancient_fantasy", "宫斗": "ancient_fantasy",
    "穿越": "ancient_fantasy", "历史": "ancient_fantasy", "神话": "ancient_fantasy",
    # 都市情感
    "都市": "urban_romance", "现代": "urban_romance", "情感": "urban_romance",
    "爱情": "urban_romance", "职场": "urban_romance", "家庭": "urban_romance",

[115 more lines in file. Use offset=460 to continue.]


现在我需要在 `pastoral_healing` 预设之后添加 4 个新的预设。让我找到确切的插入点：

            "transition_style": {
                "default": "淡入淡出", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "镜头间转场方式。淡入淡出最柔和。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },

    "youth_campus": {
        "name": "青春校园",
        "icon": "🎓",
        "description": "清新明快，自然光，青春活力，明亮色调，轻松节奏",
        "params": {
            "character_consistency": {
                "default": 0.88, "min": 0.8, "max": 0.95, "step": 0.05,
                "label": "角色一致性",
                "desc": "角色统一度。校园剧角色需辨识度",
            },
            "style_strength": {
                "default": 0.7, "min": 0.55, "max": 0.85, "step": 0.05,
                "label": "风格强度",
                "desc": "风格化程度。校园剧偏清新写实",
            },
            "image_quality": {
                "default": 0.9, "min": 0.7, "max": 1.0, "step": 0.05,
                "label": "图片质量",
                "desc": "画面精细度",
            },
            "scene_detail_level": {
                "default": 0.65, "min": 0.5, "max": 0.8, "step": 0.05,
                "label": "场景细节",
                "desc": "场景细节。校园场景适度即可",
            },
            "color_tone": {
                "default": "粉紫色调", "min": None, "max": None, "step": None,
                "label": "色调",
                "desc": "画面色调。粉紫调营造青春浪漫感",
                "options": ["暖金色调", "冷蓝色调", "暗青色调", "暖黄色调", "粉紫色调", "水墨灰调"],
            },
            "light_effect": {
                "default": "自然漫射", "min": None, "max": None, "step": None,
                "label": "光影效果",
                "desc": "光影风格。校园剧以自然光为主",
                "options": ["柔和侧光", "逆光剪影", "顶光戏剧", "自然漫射", "烛光暖照", "月光清冷"],
            },
            "negative_prompt_character": {
                "default": _build_neg("youth_campus", "character"),
                "label": "角色负面提示词",
                "desc": "角色图排除的元素。校园角色需排除老年严肃形象",
                "img_type": "character",
            },
            "negative_prompt_scene": {
                "default": _build_neg("youth_campus", "scene"),
                "label": "场景负面提示词",
                "desc": "场景图排除的元素。校园场景需排除阴暗破旧",
                "img_type": "scene",
            },
            "negative_prompt_shot": {
                "default": _build_neg("youth_campus", "shot"),
                "label": "镜头负面提示词",
                "desc": "镜头参考图排除的元素。校园镜头需排除压抑氛围",
                "img_type": "shot",
            },
            "slow_motion_ratio": {
                "default": 1.5, "min": 1.0, "max": 2.0, "step": 0.5,
                "label": "慢放倍率",
                "desc": "慢放程度。校园剧节奏轻松自然",
            },
            "dynamic_amplitude": {
                "default": 0.1, "min": 0.05, "max": 0.2, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。校园剧可适度晃动增加活力感",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "缓慢平移", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。校园剧以平稳运动为主。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "交叉溶解", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "转场方式。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },

    "sci_fi_future": {
        "name": "科幻未来",
        "icon": "🚀",
        "description": "赛博朋克，冷蓝霓虹，高科技质感，快速切换，视觉冲击",
        "params": {
            "character_consistency": {
                "default": 0.85, "min": 0.75, "max": 0.95, "step": 0.05,
                "label": "角色一致性",
                "desc": "角色统一度。科幻剧可稍低允许造型变化",
            },
            "style_strength": {
                "default": 0.9, "min": 0.75, "max": 0.95, "step": 0.05,
                "label": "风格强度",
                "desc": "风格化程度。科幻需要强风格化营造未来感",
            },
            "image_quality": {
                "default": 0.95, "min": 0.8, "max": 1.0, "step": 0.05,
                "label": "图片质量",
                "desc": "画面精细度。科幻需要高质量细节",
            },
            "scene_detail_level": {
                "default": 0.85, "min": 0.7, "max": 0.95, "step": 0.05,
                "label": "场景细节",
                "desc": "场景细节。科幻场景需要丰富的科技细节",
            },
            "color_tone": {
                "default": "冷蓝色调", "min": None, "max": None, "step": None,
                "label": "色调",
                "desc": "画面色调。冷蓝调营造赛博朋克感",
                "options": ["暖金色调", "冷蓝色调", "暗青色调", "暖黄色调", "粉紫色调", "水墨灰调"],
            },
            "light_effect": {
                "default": "逆光剪影", "min": None, "max": None, "step": None,
                "label": "光影效果",
                "desc": "光影风格。逆光剪影增强未来神秘感",
                "options": ["柔和侧光", "逆光剪影", "顶光戏剧", "自然漫射", "烛光暖照", "月光清冷"],
            },
            "negative_prompt_character": {
                "default": _build_neg("sci_fi_future", "character"),
                "label": "角色负面提示词",
                "desc": "角色图排除的元素。科幻角色需排除古代传统元素",
                "img_type": "character",
            },
            "negative_prompt_scene": {
                "default": _build_neg("sci_fi_future", "scene"),
                "label": "场景负面提示词",
                "desc": "场景图排除的元素。科幻场景需排除田园古代建筑",
                "img_type": "scene",
            },
            "negative_prompt_shot": {
                "default": _build_neg("sci_fi_future", "shot"),
                "label": "镜头负面提示词",
                "desc": "镜头参考图排除的元素。科幻镜头需排除慢节奏静态画面",
                "img_type": "shot",
            },
            "slow_motion_ratio": {
                "default": 1.0, "min": 1.0, "max": 2.0, "step": 0.5,
                "label": "慢放倍率",
                "desc": "慢放程度。科幻节奏偏快，关键瞬间才慢放",
            },
            "dynamic_amplitude": {
                "default": 0.2, "min": 0.1, "max": 0.3, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。科幻可适度晃动增加动感",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "快速推进", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。科幻适合快速推进增加紧迫感。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "闪白转场", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "转场方式。科幻适合闪白增加科技感。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },

    "war_epic": {
        "name": "战争史诗",
        "icon": "⚔",
        "description": "宏大场面，暗金色调，戏剧光影，快速推进，紧张节奏",
        "params": {
            "character_consistency": {
                "default": 0.85, "min": 0.75, "max": 0.95, "step": 0.05,
                "label": "角色一致性",
                "desc": "角色统一度。战争剧可稍低允许战损变化",
            },
            "style_strength": {
                "default": 0.85, "min": 0.7, "max": 0.95, "step": 0.05,
                "label": "风格强度",
                "desc": "风格化程度。战争剧需要强风格化营造史诗感",
            },
            "image_quality": {
                "default": 0.95, "min": 0.8, "max": 1.0, "step": 0.05,
                "label": "图片质量",
                "desc": "画面精细度。战争剧需要高质量大场面",
            },
            "scene_detail_level": {
                "default": 0.85, "min": 0.7, "max": 0.95, "step": 0.05,
                "label": "场景细节",
                "desc": "场景细节。战争场景需要丰富的战场细节",
            },
            "color_tone": {
                "default": "暖金色调", "min": None, "max": None, "step": None,
                "label": "色调",
                "desc": "画面色调。暖金调营造史诗厚重感",
                "options": ["暖金色调", "冷蓝色调", "暗青色调", "暖黄色调", "粉紫色调", "水墨灰调"],
            },
            "light_effect": {
                "default": "顶光戏剧", "min": None, "max": None, "step": None,
                "label": "光影效果",
                "desc": "光影风格。戏剧性光影增强战争紧张感",
                "options": ["柔和侧光", "逆光剪影", "顶光戏剧", "自然漫射", "烛光暖照", "月光清冷"],
            },
            "negative_prompt_character": {
                "default": _build_neg("war_epic", "character"),
                "label": "角色负面提示词",
                "desc": "角色图排除的元素。战争角色需排除柔弱轻松形象",
                "img_type": "character",
            },
            "negative_prompt_scene": {
                "default": _build_neg("war_epic", "scene"),
                "label": "场景负面提示词",
                "desc": "场景图排除的元素。战争场景需排除宁静温馨",
                "img_type": "scene",
            },
            "negative_prompt_shot": {
                "default": _build_neg("war_epic", "shot"),
                "label": "镜头负面提示词",
                "desc": "镜头参考图排除的元素。战争镜头需排除慢节奏柔和光线",
                "img_type": "shot",
            },
            "slow_motion_ratio": {
                "default": 1.5, "min": 1.0, "max": 2.5, "step": 0.5,
                "label": "慢放倍率",
                "desc": "慢放程度。战争关键瞬间需慢放增强冲击力",
            },
            "dynamic_amplitude": {
                "default": 0.2, "min": 0.1, "max": 0.3, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。战争剧晃动增加临场感",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "快速推进", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。战争适合快速推进和跟拍。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "硬切", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "转场方式。战争适合硬切增加紧张感。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },

    "comedy_farce": {
        "name": "喜剧闹剧",
        "icon": "😂",
        "description": "明亮欢快，暖黄色调，自然光线，轻松节奏，夸张表情",
        "params": {
            "character_consistency": {
                "default": 0.85, "min": 0.75, "max": 0.95, "step": 0.05,
                "label": "角色一致性",
                "desc": "角色统一度。喜剧可稍低允许夸张表情变化",
            },
            "style_strength": {
                "default": 0.65, "min": 0.5, "max": 0.8, "step": 0.05,
                "label": "风格强度",
                "desc": "风格化程度。喜剧偏写实，不需要太强风格化",
            },
            "image_quality": {
                "default": 0.85, "min": 0.7, "max": 1.0, "step": 0.05,
                "label": "图片质量",
                "desc": "画面精细度",
            },
            "scene_detail_level": {
                "default": 0.6, "min": 0.4, "max": 0.8, "step": 0.05,
                "label": "场景细节",
                "desc": "场景细节。喜剧场景适度即可，重点在人物表演",
            },
            "color_tone": {
                "default": "暖黄色调", "min": None, "max": None, "step": None,
                "label": "色调",
                "desc": "画面色调。暖黄调营造欢快轻松感",
                "options": ["暖金色调", "冷蓝色调", "暗青色调", "暖黄色调", "粉紫色调", "水墨灰调"],
            },
            "light_effect": {
                "default": "自然漫射", "min": None, "max": None, "step": None,
                "label": "光影效果",
                "desc": "光影风格。喜剧以明亮自然光为主",
                "options": ["柔和侧光", "逆光剪影", "顶光戏剧", "自然漫射", "烛光暖照", "月光清冷"],
            },
            "negative_prompt_character": {
                "default": _build_neg("comedy_farce", "character"),
                "label": "角色负面提示词",
                "desc": "角色图排除的元素。喜剧角色需排除严肃恐怖表情",
                "img_type": "character",
            },
            "negative_prompt_scene": {
                "default": _build_neg("comedy_farce", "scene"),
                "label": "场景负面提示词",
                "desc": "场景图排除的元素。喜剧场景需排除恐怖阴暗环境",
                "img_type": "scene",
            },
            "negative_prompt_shot": {
                "default": _build_neg("comedy_farce", "shot"),
                "label": "镜头负面提示词",
                "desc": "镜头参考图排除的元素。喜剧镜头需排除紧张压抑氛围",
                "img_type": "shot",
            },
            "slow_motion_ratio": {
                "default": 1.0, "min": 1.0, "max": 1.5, "step": 0.5,
                "label": "慢放倍率",
                "desc": "慢放程度。喜剧节奏明快，不需慢放",
            },
            "dynamic_amplitude": {
                "default": 0.15, "min": 0.05, "max": 0.25, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。喜剧可适度晃动增加活泼感",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "跟拍", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。喜剧适合跟拍增加临场感。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "硬切", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "转场方式。喜剧适合硬切保持节奏明快。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },
}

            "transition_style": {
                "default": "淡入淡出", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "镜头间转场方式。淡入淡出最柔和。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },
}


现在，扩展 `STYLE_KEYWORD_MAP` 以包含新的风格：


                "desc": "慢放程度。战争关键瞬间需慢放增强冲击力",
            },
            "dynamic_amplitude": {
                "default": 0.2, "min": 0.1, "max": 0.3, "step": 0.05,
                "label": "画面晃动",
                "desc": "晃动幅度。战争剧晃动增加临场感",
            },
            "frame_rate": {
                "default": 24, "min": 24, "max": 30, "step": 6,
                "label": "帧率",
                "desc": "视频帧率",
            },
            "camera_type": {
                "default": "快速推进", "min": None, "max": None, "step": None,
                "label": "运镜方式",
                "desc": "镜头运动。战争适合快速推进和跟拍。可从推荐中选择或自行输入",
                "options": CAMERA_TYPE_OPTIONS,
                "allow_custom": True,
            },
            "transition_style": {
                "default": "硬切", "min": None, "max": None, "step": None,
                "label": "镜头转场",
                "desc": "转场方式。战争适合硬切增加紧张感。可从推荐中选择或自行输入",
                "options": TRANSITION_STYLE_OPTIONS,
                "allow_custom": True,
            },
        },
    },

    "comedy_farce": {

[213 more lines in file. Use offset=710 to continue.]


exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "STYLE_KEYWORD_MAP" style_presets.py


800:STYLE_KEYWORD_MAP = {
849:    for keyword, preset_id in STYLE_KEYWORD_MAP.items():


STYLE_KEYWORD_MAP = {
    # 古风仙侠
    "古风": "ancient_fantasy", "仙侠": "ancient_fantasy", "修仙": "ancient_fantasy",
    "武侠": "ancient_fantasy", "玄幻": "ancient_fantasy", "宫斗": "ancient_fantasy",
    "穿越": "ancient_fantasy", "历史": "ancient_fantasy", "神话": "ancient_fantasy",
    # 都市情感
    "都市": "urban_romance", "现代": "urban_romance", "情感": "urban_romance",
    "爱情": "urban_romance", "职场": "urban_romance", "家庭": "urban_romance",
    "校园": "urban_romance", "青春": "urban_romance", "婆媳": "urban_romance",
    # 悬疑惊悚
    "悬疑": "suspense_thriller", "惊悚": "suspense_thriller", "恐怖": "suspense_thriller",
    "推理": "suspense_thriller", "犯罪": "suspense_thriller", "刑侦": "suspense_thriller",
    "暗黑": "suspense_thriller", "复仇": "suspense_thriller",
    # 田园治愈
    "田园": "pastoral_healing", "治愈": "pastoral_healing", "温馨": "pastoral_healing",
    "乡村": "pastoral_healing", "日常": "pastoral_healing", "慢生活": "pastoral_healing",
    "美食": "pastoral_healing", "萌宠": "pastoral_healing",
}

def get_preset(preset_id: str) -> dict:
    """获取指定风格预设"""
    return STYLE_PRESETS.get(preset_id)

def get_all_presets() -> dict:
    """获取所有风格预设(含参数详情)"""
    result = {}
    for pid, preset in STYLE_PRESETS.items():
        result[pid] = {

[93 more lines in file. Use offset=830 to continue.]


edit

STYLE_KEYWORD_MAP = {
    # 古风仙侠
    "古风": "ancient_fantasy", "仙侠": "ancient_fantasy", "修仙": "ancient_fantasy",
    "武侠": "ancient_fantasy", "玄幻": "ancient_fantasy", "宫斗": "ancient_fantasy",
    "穿越": "ancient_fantasy", "历史": "ancient_fantasy", "神话": "ancient_fantasy",
    # 都市情感
    "都市": "urban_romance", "现代": "urban_romance", "情感": "urban_romance",
    "爱情": "urban_romance", "职场": "urban_romance", "家庭": "urban_romance",
    "婆媳": "urban_romance", "婚恋": "urban_romance", "甜宠": "urban_romance",
    # 悬疑惊悚
    "悬疑": "suspense_thriller", "惊悚": "suspense_thriller", "恐怖": "suspense_thriller",
    "推理": "suspense_thriller", "犯罪": "suspense_thriller", "刑侦": "suspense_thriller",
    "暗黑": "suspense_thriller", "复仇": "suspense_thriller", "密室": "suspense_thriller",
    # 田园治愈
    "田园": "pastoral_healing", "治愈": "pastoral_healing", "温馨": "pastoral_healing",
    "乡村": "pastoral_healing", "日常": "pastoral_healing", "慢生活": "pastoral_healing",
    "美食": "pastoral_healing", "萌宠": "pastoral_healing",
    # 青春校园
    "校园": "youth_campus", "青春": "youth_campus", "校园恋爱": "youth_campus",
    "学生": "youth_campus", "同桌": "youth_campus", "毕业": "youth_campus",
    "少年": "youth_campus", "少女": "youth_campus", "初恋": "youth_campus",
    # 科幻未来
    "科幻": "sci_fi_future", "未来": "sci_fi_future", "赛博": "sci_fi_future",
    "赛博朋克": "sci_fi_future", "机甲": "sci_fi_future", "太空": "sci_fi_future",
    "外星": "sci_fi_future", "AI": "sci_fi_future", "人工智能": "sci_fi_future",
    # 战争史诗
    "战争": "war_epic", "史诗": "war_epic", "军事": "war_epic",
    "抗战": "war_epic", "战场": "war_epic", "英雄": "war_epic",
    "将军": "war_epic", "兵": "war_epic", "谍战": "war_epic",
    # 喜剧闹剧
    "喜剧": "comedy_farce", "搞笑": "comedy_farce", "闹剧": "comedy_farce",
    "幽默": "comedy_farce", "沙雕": "comedy_farce", "欢乐": "comedy_farce",
    "吐槽": "comedy_farce", "反转": "comedy_farce", "荒诞": "comedy_farce",
}

STYLE_KEYWORD_MAP = {
    # 古风仙侠
    "古风": "ancient_fantasy", "仙侠": "ancient_fantasy", "修仙": "ancient_fantasy",
    "武侠": "ancient_fantasy", "玄幻": "ancient_fantasy", "宫斗": "ancient_fantasy",
    "穿越": "ancient_fantasy", "历史": "ancient_fantasy", "神话": "ancient_fantasy",
    # 都市情感
    "都市": "urban_romance", "现代": "urban_romance", "情感": "urban_romance",
    "爱情": "urban_romance", "职场": "urban_romance", "家庭": "urban_romance",
    "校园": "urban_romance", "青春": "urban_romance", "婆媳": "urban_romance",
    # 悬疑惊悚
    "悬疑": "suspense_thriller", "惊悚": "suspense_thriller", "恐怖": "suspense_thriller",
    "推理": "suspense_thriller", "犯罪": "suspense_thriller", "刑侦": "suspense_thriller",
    "暗黑": "suspense_thriller", "复仇": "suspense_thriller",
    # 田园治愈
    "田园": "pastoral_healing", "治愈": "pastoral_healing", "温馨": "pastoral_healing",
    "乡村": "pastoral_healing", "日常": "pastoral_healing", "慢生活": "pastoral_healing",
    "美食": "pastoral_healing", "萌宠": "pastoral_healing",
}


     ...
 804     "穿越": "ancient_fantasy", "历史": "ancient_fantasy", "神话": "ancient_fantasy",
 805     # 都市情感
 806     "都市": "urban_romance", "现代": "urban_romance", "情感": "urban_romance",
 807     "爱情": "urban_romance", "职场": "urban_romance", "家庭": "urban_romance",
-808     "校园": "urban_romance", "青春": "urban_romance", "婆媳": "urban_romance",
+808     "婆媳": "urban_romance", "婚恋": "urban_romance", "甜宠": "urban_romance",
 809     # 悬疑惊悚
 810     "悬疑": "suspense_thriller", "惊悚": "suspense_thriller", "恐怖": "suspense_thriller",
 811     "推理": "suspense_thriller", "犯罪": "suspense_thriller", "刑侦": "suspense_thriller",
-812     "暗黑": "suspense_thriller", "复仇": "suspense_thriller",
+812     "暗黑": "suspense_thriller", "复仇": "suspense_thriller", "密室": "suspense_thriller",
 813     # 田园治愈
 814     "田园": "pastoral_healing", "治愈": "pastoral_healing", "温馨": "pastoral_healing",
 815     "乡村": "pastoral_healing", "日常": "pastoral_healing", "慢生活": "pastoral_healing",
 816     "美食": "pastoral_healing", "萌宠": "pastoral_healing",
+817     # 青春校园
+818     "校园": "youth_campus", "青春": "youth_campus", "校园恋爱": "youth_campus",
+819     "学生": "youth_campus", "同桌": "youth_campus", "毕业": "youth_campus",
+820     "少年": "youth_campus", "少女": "youth_campus", "初恋": "youth_campus",
+821     # 科幻未来
+822     "科幻": "sci_fi_future", "未来": "sci_fi_future", "赛博": "sci_fi_future",
+823     "赛博朋克": "sci_fi_future", "机甲": "sci_fi_future", "太空": "sci_fi_future",
+824     "外星": "sci_fi_future", "AI": "sci_fi_future", "人工智能": "sci_fi_future",
+825     # 战争史诗
+826     "战争": "war_epic", "史诗": "war_epic", "军事": "war_epic",
+827     "抗战": "war_epic", "战场": "war_epic", "英雄": "war_epic",
+828     "将军": "war_epic", "兵": "war_epic", "谍战": "war_epic",
+829     # 喜剧闹剧
+830     "喜剧": "comedy_farce", "搞笑": "comedy_farce", "闹剧": "comedy_farce",
+831     "幽默": "comedy_farce", "沙雕": "comedy_farce", "欢乐": "comedy_farce",
+832     "吐槽": "comedy_farce", "反转": "comedy_farce", "荒诞": "comedy_farce",
 817 }
 818
 819
 820 def get_preset(preset_id: str) -> dict:
     ...


好的。现在我将添加一个辅助函数，用于为每个镜头推荐摄像机/转场参数。接下来，我还会添加一个 `SHOT_CAMERA_RECOMMENDATIONS` 字典，用于将镜头类型映射到推荐的摄像机移动方式：

# ============================================================
# 镜头类型 → 运镜/转场推荐 (v3.21)
# ============================================================
SHOT_CAMERA_RECOMMENDATIONS = {
    "特写": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "特写镜头适合缓慢推进突出细节，交叉溶解过渡柔和"},
    "近景": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "近景适合缓慢推进聚焦人物表情"},
    "中景": {"camera_type": "缓慢平移", "transition_style": "交叉溶解", "reason": "中景适合平移展示人物动作"},
    "全景": {"camera_type": "缓慢拉远", "transition_style": "淡入淡出", "reason": "全景适合拉远展示环境全貌"},
    "远景": {"camera_type": "缓慢平移", "transition_style": "淡入淡出", "reason": "远景适合平移展示大场景"},
    "跟拍": {"camera_type": "跟拍", "transition_style": "硬切", "reason": "跟拍镜头适合跟踪运动，硬切保持节奏"},
    "主观": {"camera_type": "主观视角", "transition_style": "模糊转场", "reason": "主观镜头适合第一人称视角"},
    "过肩": {"camera_type": "过肩镜头", "transition_style": "硬切", "reason": "过肩镜头适合对话场景"},
    "仰拍": {"camera_type": "低角度仰拍", "transition_style": "硬切", "reason": "仰拍适合突出角色威严感"},
    "俯拍": {"camera_type": "高角度俯拍", "transition_style": "淡入淡出", "reason": "俯拍适合展示全局布局"},
    "default": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "默认推荐"},
}

def recommend_shot_camera(shot_type: str, style_preset_id: str = None) -> dict:
    """v3.21: 根据镜头类型和风格预设推荐运镜/转场参数

    优先使用镜头类型推荐，如果风格预设也有默认值则作为备选
    """
    # 根据镜头类型获取推荐
    shot_rec = SHOT_CAMERA_RECOMMENDATIONS.get(shot_type, SHOT_CAMERA_RECOMMENDATIONS["default"])

    # 如果有风格预设，获取其默认值作为备选
    style_default = None
    if style_preset_id:
        preset = STYLE_PRESETS.get(style_preset_id)
        if preset:
            style_camera = preset["params"].get("camera_type", {}).get("default")
            style_transition = preset["params"].get("transition_style", {}).get("default")
            if style_camera and style_transition:
                style_default = {"camera_type": style_camera, "transition_style": style_transition}

    return {
        "camera_type": shot_rec["camera_type"],
        "transition_style": shot_rec["transition_style"],
        "reason": shot_rec["reason"],
        "style_default": style_default,  # 风格预设的默认值，用户可参考
        "all_camera_options": CAMERA_TYPE_OPTIONS,
        "all_transition_options": TRANSITION_STYLE_OPTIONS,
    }

STYLE_KEYWORD_MAP = {


edits[0] and edits[1] overlap in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/style_presets.py. Merge them into one edit or target disjoint regions.


     ...
 796
 797 # ============================================================
 798 # 风格关键词 → 预设映射 (创作官推荐用)
 799 # ============================================================
+800 # ============================================================
+801 # 镜头类型 → 运镜/转场推荐 (v3.21)
+802 # ============================================================
+803 SHOT_CAMERA_RECOMMENDATIONS = {
+804     "特写": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "特写镜头适合缓慢推进突出细节，交叉溶解过渡柔和"},
+805     "近景": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "近景适合缓慢推进聚焦人物表情"},
+806     "中景": {"camera_type": "缓慢平移", "transition_style": "交叉溶解", "reason": "中景适合平移展示人物动作"},
+807     "全景": {"camera_type": "缓慢拉远", "transition_style": "淡入淡出", "reason": "全景适合拉远展示环境全貌"},
+808     "远景": {"camera_type": "缓慢平移", "transition_style": "淡入淡出", "reason": "远景适合平移展示大场景"},
+809     "跟拍": {"camera_type": "跟拍", "transition_style": "硬切", "reason": "跟拍镜头适合跟踪运动，硬切保持节奏"},
+810     "主观": {"camera_type": "主观视角", "transition_style": "模糊转场", "reason": "主观镜头适合第一人称视角"},
+811     "过肩": {"camera_type": "过肩镜头", "transition_style": "硬切", "reason": "过肩镜头适合对话场景"},
+812     "仰拍": {"camera_type": "低角度仰拍", "transition_style": "硬切", "reason": "仰拍适合突出角色威严感"},
+813     "俯拍": {"camera_type": "高角度俯拍", "transition_style": "淡入淡出", "reason": "俯拍适合展示全局布局"},
+814     "default": {"camera_type": "缓慢推进", "transition_style": "交叉溶解", "reason": "默认推荐"},
+815 }
+816
+817
+818 def recommend_shot_camera(shot_type: str, style_preset_id: str = None) -> dict:
+819     """v3.21: 根据镜头类型和风格预设推荐运镜/转场参数
+820
+821     优先使用镜头类型推荐，如果风格预设也有默认值则作为备选
+822     """
+823     # 根据镜头类型获取推荐
+824     shot_rec = SHOT_CAMERA_RECOMMENDATIONS.get(shot_type, SHOT_CAMERA_RECOMMENDATIONS["default"])
+825
+826     # 如果有风格预设，获取其默认值作为备选
+827     style_default = None
+828     if style_preset_id:
+829         preset = STYLE_PRESETS.get(style_preset_id)
+830         if preset:
+831             style_camera = preset["params"].get("camera_type", {}).get("default")
+832             style_transition = preset["params"].get("transition_style", {}).get("default")
+833             if style_camera and style_transition:
+834                 style_default = {"camera_type": style_camera, "transition_style": style_transition}
+835
+836     return {
+837         "camera_type": shot_rec["camera_type"],
+838         "transition_style": shot_rec["transition_style"],
+839         "reason": shot_rec["reason"],
+840         "style_default": style_default,  # 风格预设的默认值，用户可参考
+841         "all_camera_options": CAMERA_TYPE_OPTIONS,
+842         "all_transition_options": TRANSITION_STYLE_OPTIONS,
+843     }
+844
+845
 800 STYLE_KEYWORD_MAP = {
 801     # 古风仙侠
 802     "古风": "ancient_fantasy", "仙侠": "ancient_fantasy", "修仙": "ancient_fantasy",
 803     "武侠": "ancient_fantasy", "玄幻": "ancient_fantasy", "宫斗": "ancient_fantasy",
     ...


好的。现在我来为镜头级摄像机推荐添加 API 端点，并更新 `api_server.py` 的版本：

@app.post("/api/style-presets/shot-camera-recommend")
async def recommend_shot_camera_api(request: Request):
    """v3.21: 根据镜头类型推荐运镜/转场参数"""
    body = await request.json()
    shot_type = body.get("shot_type", "default")
    style_preset_id = body.get("style_preset_id", None)
    from style_presets import recommend_shot_camera
    result = recommend_shot_camera(shot_type, style_preset_id)
    return {"success": True, "recommendation": result}

@app.post("/api/style-presets/{preset_id}/validate")


Found 2 occurrences of edits[1] in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py. Each oldText must be unique. Please provide more context to make it unique.


exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n '"version": "3.20"' api_server.py


177:        "version": "3.20",
1067:        "version": "3.20",


app = FastAPI(
    title="慢剧生成工作台API",
    description="个人慢剧创作平台的API接口",
    version="3.20"
)

[1122 more lines in file. Use offset=90 to continue.]


    return {
        "service": "慢剧生成工作台API",
        "version": "3.20",
        "status": "运行中",
        "endpoints": {

[1032 more lines in file. Use offset=180 to continue.]


    return {
        "status": "healthy",
        "version": "3.20",
        "timestamp": datetime.now().isoformat(),
        "directory_checks": dir_checks,

[142 more lines in file. Use offset=1070 to continue.]


    from style_presets import recommend_preset
    result = recommend_preset(description, visual_style)
    return {"success": True, "recommendation": result}

@app.post("/api/style-presets/{preset_id}/validate")
async def validate_style_params(preset_id: str, request: Request):
    """验证用户调整的参数，钳制到允许区间"""
    body = await request.json()
    user_params = body.get("params", {})
    from style_presets import validate_and_clamp

[104 more lines in file. Use offset=1108 to continue.]


edit

        "version": "3.21",
        "status": "运行中",

        "version": "3.20",
        "status": "运行中",

        "version": "3.21",
        "timestamp":

        "version": "3.20",
        "timestamp":


Successfully replaced 4 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/api_server.py.

      ...
   84 # 初始化FastAPI应用
   85 app = FastAPI(
   86     title="慢剧生成工作台API",
   87     description="个人慢剧创作平台的API接口",
-  88     version="3.20"
+  88     version="3.21"
   89 )
   90
   91 # 添加CORS中间件
   92 app.add_middleware(
      ...
  173 async def api_root():
  174     """API根路径"""
  175     return {
  176         "service": "慢剧生成工作台API",
- 177         "version": "3.20",
+ 177         "version": "3.21",
  178         "status": "运行中",
  179         "endpoints": {
  180             "projects": "/api/projects",
  181             "project_detail": "/api/projects/{id}",
      ...
 1063         disk_usage = {"error": "无法获取磁盘使用情况"}
 1064
 1065     return {
 1066         "status": "healthy",
-1067         "version": "3.20",
+1067         "version": "3.21",
 1068         "timestamp": datetime.now().isoformat(),
 1069         "directory_checks": dir_checks,
 1070         "project_count": project_count,
 1071         "disk_usage": disk_usage
      ...
 1098     from style_presets import recommend_preset
 1099     result = recommend_preset(description, visual_style)
 1100     return {"success": True, "recommendation": result}
 1101
+1102 @app.post("/api/style-presets/shot-camera-recommend")
+1103 async def recommend_shot_camera_api(request: Request):
+1104     """v3.21: 根据镜头类型推荐运镜/转场参数"""
+1105     body = await request.json()
+1106     shot_type = body.get("shot_type", "default")
+1107     style_preset_id = body.get("style_preset_id", None)
+1108     from style_presets import recommend_shot_camera
+1109     result = recommend_shot_camera(shot_type, style_preset_id)
+1110     return {"success": True, "recommendation": result}
+1111
+1112
 1102 @app.post("/api/style-presets/{preset_id}/validate")
 1103 async def validate_style_params(preset_id: str, request: Request):
 1104     """验证用户调整的参数，钳制到允许区间"""
 1105     body = await request.json()
      ...


现在更新 `task_system.py` 的版本，并添加镜头级别的视频参数支持：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && head -10 user_driven_task_system.py


"""
用户主导的任务系统
版本: 3.20
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
v3.5: 创建任务配置(时长/渠道/视频模式)、删除任务、修改弹框+历史、场景/分镜修改、步骤联动、审核增强
v3.7: Agent真实调用改造 - 各步骤优先通过OpenClaw Gateway调用对应Agent(采集官/创作官/制作官/审核官),LLM直接调用作为fallback
v3.8: 恢复seedream图生图功能 - 使用官方`image`参数(非image_url),角色图/场景图/镜头参考图均支持参考图输入
v3.16: QA9项检查扩充+角色锚点注入+反派约束+结尾钩子+前端QA可视化
"""


edit

"""
用户主导的任务系统
版本: 3.21
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
v3.21: 镜头级视频参数(运镜/转场按镜头配置), 风格预设移到第二步, 8种风格预设
v3.5: 创建任务配置(时长/渠道/视频模式)、删除任务、修改弹框+历史、场景/分镜修改、步骤联动、审核增强
v3.7: Agent真实调用改造 - 各步骤优先通过OpenClaw Gateway调用对应Agent(采集官/创作官/制作官/审核官),LLM直接调用作为fallback
v3.8: 恢复seedream图生图功能 - 使用官方`image`参数(非image_url),角色图/场景图/镜头参考图均支持参考图输入
v3.16: QA9项检查扩充+角色锚点注入+反派约束+结尾钩子+前端QA可视化
"""

"""
用户主导的任务系统
版本: 3.20
功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
v3.5: 创建任务配置(时长/渠道/视频模式)、删除任务、修改弹框+历史、场景/分镜修改、步骤联动、审核增强
v3.7: Agent真实调用改造 - 各步骤优先通过OpenClaw Gateway调用对应Agent(采集官/创作官/制作官/审核官),LLM直接调用作为fallback
v3.8: 恢复seedream图生图功能 - 使用官方`image`参数(非image_url),角色图/场景图/镜头参考图均支持参考图输入
v3.16: QA9项检查扩充+角色锚点注入+反派约束+结尾钩子+前端QA可视化
"""


Successfully replaced 1 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py.

    1 """
    2 用户主导的任务系统
-   3 版本: 3.20
+   3 版本: 3.21
    4 功能: 支持分步执行、结果修改、图/文生视频切换、采集入库、字幕添加、视频拼接
+   5 v3.21: 镜头级视频参数(运镜/转场按镜头配置), 风格预设移到第二步, 8种风格预设
    5 v3.5: 创建任务配置(时长/渠道/视频模式)、删除任务、修改弹框+历史、场景/分镜修改、步骤联动、审核增强
    6 v3.7: Agent真实调用改造 - 各步骤优先通过OpenClaw Gateway调用对应Agent(采集官/创作官/制作官/审核官),LLM直接调用作为fallback
    7 v3.8: 恢复seedream图生图功能 - 使用官方`image`参数(非image_url),角色图/场景图/镜头参考图均支持参考图输入
    8 v3.16: QA9项检查扩充+角色锚点注入+反派约束+结尾钩子+前端QA可视化
      ...


现在我来更新 `_get_style_params` 以支持镜头级别的参数覆盖。关键的改动是：当生成视频时，如果镜头有 `shot_camera_type` / `shot_transition_style` 覆盖，则使用它们而不是全局预设：


        task_data["progress"] = 60
        task_data["updated_at"] = datetime.now().isoformat()

        # v3.19: 从风格预设获取视频参数
        style_video_params = self._get_style_params(task_data, "video")

        # 记录用户动作
        task_data["user_actions"].append({
            "action": "execute_produce",
            "timestamp": datetime.now().isoformat(),
            "description": "用户执行制作步骤(图生视频模式)"
        })

        # 调用真实视频生成API
        task_dir = Path(task_data["files"]["task_dir"])
        videos_dir = task_dir / "videos"
        images_dir = task_dir / "images"
        videos_dir.mkdir(parents=True, exist_ok=True)
        images_dir.mkdir(parents=True, exist_ok=True)

        # 从脚本结果中提取镜头和角色
        script_data = task_data.get("results", {}).get("script", {})
        scenes = script_data.get("scenes", []) if isinstance(script_data, dict) else []
        characters = script_data.get("characters", []) if isinstance(script_data, dict) else []

        produced_shots = []
        video_files = []
        img2vid_count = 0
        text2vid_count = 0

[2011 more lines in file. Use offset=2015 to continue.]


                    # 【v3.6修复】对白内容必须传入视频生成prompt,确保角色说话内容与画面匹配
                    dialogue = shot.get("dialogue", "")
                    if dialogue and dialogue not in video_prompt:
                        video_prompt = video_prompt + ",角色说:" + dialogue

                    # 匹配参考图
                    ref_image = self._resolve_shot_ref_image(shot, scene, characters)
                    shot_mode = get_shot_mode(shot_id, ref_image and Path(ref_image).exists())

                    try:
                        if shot_mode == "img2vid" and ref_image and Path(ref_image).exists():
                            logger.info(f"  🎨 图生视频 镜头{shot_id}/{total_shots}: ref={Path(ref_image).name}")
                            try:
                                result = generate_video_from_image(
                                    image_path=ref_image,
                                    prompt=video_prompt,
                                    model=_video_model,
                                    ratio="9:16",
                                    duration=shot.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,
                                )
                                img2vid_count += 1
                            except Exception as img_err:
                                # 图生视频失败(如敏感内容),降级到文生视频
                                if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
                                    logger.warning(f"  ⚠ 图生视频触发敏感内容检测,降级到文生视频: 镜头{shot_id}")
                                    shot_mode = "text2vid_fallback"
                                else:
                                    raise

                        if shot_mode.startswith("text2vid"):
                            logger.info(f"  ✏ 文生视频 镜头{shot_id}/{total_shots}")
                            # 文生视频需要更详细的描述
                            text_prompt = video_prompt
                            if shot_mode == "text2vid":
                                # 强制文生模式时,补充角色和场景描述
                                desc_parts = []
                                if shot.get("description"):
                                    desc_parts.append(shot["description"])
                                for c in characters:
                                    if c.get("name") and c.get("name") in text_prompt:
                                        desc_parts.append(f"角色{c['name']}: {c.get('description','')}")
                                if scene.get("location"):
                                    desc_parts.append(f"场景: {scene['location']}")
                                if len(desc_parts) > 1:
                                    text_prompt = ",".join(desc_parts) + "," + video_prompt
                            result = generate_video(
                                prompt=text_prompt,
                                model=_video_model,
                                ratio="9:16",
                                duration=shot.get("duration_seconds", 5),
                                download_dir=str(videos_dir),
                                timeout_wait=600,
                            )
                            text2vid_count += 1

                        shot_result = {
                            "shot_id": shot_id,
                            "status": "succeeded",
                            "video_url": result.get("video_url", ""),
                            "local_path": result.get("local_path", ""),
                            "file_size": result.get("file_size", 0),
                            "duration": result.get("duration"),
                            "resolution": result.get("resolution"),
                            "ref_image": ref_image,  # 保留参考图路径,即使降级为text2vid_fallback也方便前端展示
                            "mode": shot_mode,
                        }
                        # 尝试添加字幕
                        if result.get("local_path") and shot.get("dialogue"):
                            try:
                                subtitled = self._add_subtitles_to_video(result["local_path"], shot)
                                if subtitled:
                                    # 字幕版替换原版：从video_files移除原版，加入字幕版
                                    orig_path = result["local_path"]
                                    if orig_path in video_files:
                                        video_files.remove(orig_path)
                                    shot_result["local_path"] = subtitled
                                    shot_result["has_subtitles"] = True

[1906 more lines in file. Use offset=2120 to continue.]


我需要修改视频生成部分，以合并镜头级别的相机/转场覆盖。关键的改动是在生成每个镜头的视频时，检查镜头是否包含 `shot_camera_type` / `shot_transition_style`，并用它们来覆盖全局的 `style_video_params`：

                    # 匹配参考图
                    ref_image = self._resolve_shot_ref_image(shot, scene, characters)
                    shot_mode = get_shot_mode(shot_id, ref_image and Path(ref_image).exists())

                    # v3.21: 镜头级视频参数 - 如果镜头有独立的运镜/转场设置则覆盖全局
                    shot_video_params = dict(style_video_params)
                    if shot.get("shot_camera_type"):
                        shot_video_params["camera_type"] = shot["shot_camera_type"]
                    if shot.get("shot_transition_style"):
                        shot_video_params["transition_style"] = shot["shot_transition_style"]

                    try:

                    # 匹配参考图
                    ref_image = self._resolve_shot_ref_image(shot, scene, characters)
                    shot_mode = get_shot_mode(shot_id, ref_image and Path(ref_image).exists())

                    try:

                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **shot_video_params,

                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,


Found 3 occurrences of edits[1] in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py. Each oldText must be unique. Please provide more context to make it unique.


exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "style_video_params" user_driven_task_system.py


1989:        style_video_params = self._get_style_params(task_data, "video")
2061:                                    **style_video_params,
2752:                                    **style_video_params,
2761:                                    **style_video_params,
3269:                        **style_video_params,


我需要更仔细地处理这个问题。有多个地方使用了 `style_video_params`。让我查看每个地方，并应用镜头级别的覆盖。最简洁的方法是：在获取 `style_video_params` 后立即创建一个辅助函数，并修改每个视频生成调用点。


                                    prompt=video_prompt,
                                    model=_video_model,
                                    ratio="9:16",
                                    duration=shot.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,
                                )
                                img2vid_count += 1
                            except Exception as img_err:

[1961 more lines in file. Use offset=2065 to continue.]


edit

        # v3.19: 从风格预设获取视频参数
        style_video_params = self._get_style_params(task_data, "video")

        # v3.21: 镜头级视频参数辅助函数
        def _merge_shot_video_params(shot_data: dict) -> dict:
            """合并全局视频参数和镜头级覆盖"""
            merged = dict(style_video_params)
            if shot_data.get("shot_camera_type"):
                merged["camera_type"] = shot_data["shot_camera_type"]
            if shot_data.get("shot_transition_style"):
                merged["transition_style"] = shot_data["shot_transition_style"]
            return merged

                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **_merge_shot_video_params(shot),

                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,
                                )
                                img2vid_count += 1


Successfully replaced 2 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py.

      ...
 1987
 1988         # v3.19: 从风格预设获取视频参数
 1989         style_video_params = self._get_style_params(task_data, "video")
 1990
+1991         # v3.21: 镜头级视频参数辅助函数
+1992         def _merge_shot_video_params(shot_data: dict) -> dict:
+1993             """合并全局视频参数和镜头级覆盖"""
+1994             merged = dict(style_video_params)
+1995             if shot_data.get("shot_camera_type"):
+1996                 merged["camera_type"] = shot_data["shot_camera_type"]
+1997             if shot_data.get("shot_transition_style"):
+1998                 merged["transition_style"] = shot_data["shot_transition_style"]
+1999             return merged
+2000
 1991         # 记录用户动作
 1992         task_data["user_actions"].append({
 1993             "action": "execute_produce",
 1994             "timestamp": datetime.now().isoformat(),
      ...
 2056                                     model=_video_model,
 2057                                     ratio="9:16",
 2058                                     duration=shot.get("duration_seconds", 5),
 2059                                     download_dir=str(videos_dir),
-2060                                     # v3.19: 风格预设驱动视频参数
-2061                                     **style_video_params,
-2062                                 )
-2063                                 img2vid_count += 1
+2070                                     # v3.21: 镜头级风格预设驱动视频参数
+2071                                     **_merge_shot_video_params(shot),
 2064                             except Exception as img_err:
 2065                                 # 图生视频失败(如敏感内容),降级到文生视频
 2066                                 if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
 2067                                     logger.warning(f"  ⚠ 图生视频触发敏感内容检测,降级到文生视频: 镜头{shot_id}")
      ...


现在我来检查其他 `style_video_params` 的使用情况，并更新它们：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n -B2 -A2 "style_video_params" user_driven_task_system.py


1987-
1988-        # v3.19: 从风格预设获取视频参数
1989:        style_video_params = self._get_style_params(task_data, "video")
1990-
1991-        # v3.21: 镜头级视频参数辅助函数
1992-        def _merge_shot_video_params(shot_data: dict) -> dict:
1993-            """合并全局视频参数和镜头级覆盖"""
1994:            merged = dict(style_video_params)
1995-            if shot_data.get("shot_camera_type"):
1996-                merged["camera_type"] = shot_data["shot_camera_type"]
--
2758-                                    download_dir=str(videos_dir),
2759-                                    # v3.19: 风格预设驱动视频参数
2760:                                    **style_video_params,
2761-                                )
2762-                            else:
--
2767-                                    download_dir=str(videos_dir),
2768-                                    # v3.19: 风格预设驱动视频参数
2769:                                    **style_video_params,
2770-                                )
2771-
--
3275-                        download_dir=str(videos_dir),
3276-                        # v3.19: 风格预设驱动视频参数
3277:                        **style_video_params,
3278-                    )
3279-                except Exception as img_err:


我需要检查这些其他用途的上下文，以确定 `shot` 变量是否可用：


                        try:
                            ref_image = sh.get("ref_image")
                            shot_mode = "img2vid" if (ref_image and Path(ref_image).exists()) else "text2vid"
                            vp = sh.get("video_prompt", sh.get("description", ""))

                            if shot_mode == "img2vid":
                                result = generate_video_from_image(
                                    image_path=ref_image, prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,
                                )
                            else:
                                result = generate_video(
                                    prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),

[1266 more lines in file. Use offset=2768 to continue.]


        shot_mode = mode or shot_info.get("mode", "text2vid")
        ref_image = self._resolve_shot_ref_image(original_shot or {}, parent_scene or {}, characters) if original_shot else None

        try:
            if shot_mode.startswith("img2vid") and ref_image and Path(ref_image).exists():
                try:
                    result = generate_video_from_image(
                        image_path=ref_image, prompt=video_prompt, ratio="9:16",
                        model=task_data.get("video_model") if task_data.get("video_model") != "auto" else None,
                        duration=(original_shot or {}).get("duration_seconds", 5),
                        download_dir=str(videos_dir),
                        # v3.19: 风格预设驱动视频参数
                        **style_video_params,
                    )
                except Exception as img_err:
                    if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
                        logger.warning(f"单镜头重新生成: 图生视频触发敏感内容检测,降级到文生视频")
                        shot_mode = "text2vid_fallback"
                        result = generate_video(
                            prompt=video_prompt, ratio="9:16",

[749 more lines in file. Use offset=3285 to continue.]


对于第2760行和第2769行，`sh` 是镜头。对于第3277行，`original_shot` 是镜头。我来更新它们：

edit

                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **_merge_shot_video_params(sh),
                                )
                            else:
                                result = generate_video(
                                    prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **_merge_shot_video_params(sh),

                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,
                                )
                            else:
                                result = generate_video(
                                    prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.19: 风格预设驱动视频参数
                                    **style_video_params,


      ...
 2755                                     image_path=ref_image, prompt=vp,
 2756                                     model=_video_model if _video_model and _video_model != "auto" else None,
 2757                                     duration=sh.get("duration_seconds", 5),
 2758                                     download_dir=str(videos_dir),
-2759                                     # v3.19: 风格预设驱动视频参数
-2760                                     **style_video_params,
+2759                                     # v3.21: 镜头级风格预设驱动视频参数
+2760                                     **_merge_shot_video_params(sh),
 2761                                 )
 2762                             else:
 2763                                 result = generate_video(
 2764                                     prompt=vp,
 2765                                     model=_video_model if _video_model and _video_model != "auto" else None,
 2766                                     duration=sh.get("duration_seconds", 5),
 2767                                     download_dir=str(videos_dir),
-2768                                     # v3.19: 风格预设驱动视频参数
-2769                                     **style_video_params,
+2768                                     # v3.21: 镜头级风格预设驱动视频参数
+2769                                     **_merge_shot_video_params(sh),
 2770                                 )
 2771
 2772                             # 更新对应shot的视频信息
 2773                             for es in existing_shots:
      ...


现在更新第 3277 行的单镜头重新生成：

                        # v3.21: 镜头级风格预设驱动视频参数
                        **_merge_shot_video_params(original_shot or {}),

                        # v3.19: 风格预设驱动视频参数
                        **style_video_params,


      ...
 3272                         image_path=ref_image, prompt=video_prompt, ratio="9:16",
 3273                         model=task_data.get("video_model") if task_data.get("video_model") != "auto" else None,
 3274                         duration=(original_shot or {}).get("duration_seconds", 5),
 3275                         download_dir=str(videos_dir),
-3276                         # v3.19: 风格预设驱动视频参数
-3277                         **style_video_params,
+3276                         # v3.21: 镜头级风格预设驱动视频参数
+3277                         **_merge_shot_video_params(original_shot or {}),
 3278                     )
 3279                 except Exception as img_err:
 3280                     if "SensitiveContent" in str(img_err) or "sensitive" in str(img_err).lower():
 3281                         logger.warning(f"单镜头重新生成: 图生视频触发敏感内容检测,降级到文生视频")
      ...


等等 - `_merge_shot_video_params` 是在 `execute_produce` 方法内部定义的。单次镜头重新生成的方法无法访问它。让我检查一下：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "def.*regenerate.*shot\|def.*_regen\|style_video_params = self._get_style_params" user_driven_task_system.py


1989:        style_video_params = self._get_style_params(task_data, "video")
3225:    def regenerate_single_shot(self, task_id: str, shot_id: int, user_prompt: Optional[str] = None, mode: Optional[str] = None) -> Dict[str, Any]:


    def regenerate_single_shot(self, task_id: str, shot_id: int, user_prompt: Optional[str] = None, mode: Optional[str] = None) -> Dict[str, Any]:
        """重新生成单个镜头的视频"""
        task_data = self._load_task(task_id)
        if not task_data:
            return {"success": False, "message": "任务不存在"}

        video_result = task_data.get("results", {}).get("video", {})
        if not video_result:
            return {"success": False, "message": "视频结果不存在,请先执行制作步骤"}

        shots = video_result.get("shots", [])
        shot_info = None
        for s in shots:
            if s.get("shot_id") == shot_id:
                shot_info = s

[794 more lines in file. Use offset=3240 to continue.]


我明白了 - `regenerate_single_shot` 也调用了 `style_video_params`。我来看看它是如何获取的：


1989:        style_video_params = self._get_style_params(task_data, "video")
1994:            merged = dict(style_video_params)


等等，第3277行已经更新了，但`regenerate_single_shot`方法无法访问`_merge_shot_video_params`。让我检查一下：


        videos_dir.mkdir(parents=True, exist_ok=True)

        # 获取原始镜头信息
        script_data = task_data.get("results", {}).get("script", {})
        scenes = script_data.get("scenes", []) if isinstance(script_data, dict) else []
        characters = script_data.get("characters", []) if isinstance(script_data, dict) else []

        original_shot = None
        parent_scene = None
        for scene in scenes:

[776 more lines in file. Use offset=3258 to continue.]


我需要将 `_merge_shot_video_params` 移动为一个类方法，或者将其内联到 `regenerate_single_shot` 中。让我把它变成一个类方法：


        # v3.19: 从风格预设获取视频参数
        style_video_params = self._get_style_params(task_data, "video")

        # v3.21: 镜头级视频参数辅助函数
        def _merge_shot_video_params(shot_data: dict) -> dict:
            """合并全局视频参数和镜头级覆盖"""
            merged = dict(style_video_params)
            if shot_data.get("shot_camera_type"):
                merged["camera_type"] = shot_data["shot_camera_type"]
            if shot_data.get("shot_transition_style"):
                merged["transition_style"] = shot_data["shot_transition_style"]
            return merged

        # 记录用户动作
        task_data["user_actions"].append({
            "action": "execute_produce",
            "timestamp": datetime.now().isoformat(),
            "description": "用户执行制作步骤(图生视频模式)"
        })

[2026 more lines in file. Use offset=2008 to continue.]


我将 `_merge_shot_video_params` 转换为一个合适的类方法，这样它就可以从 `regenerate_single_shot` 中访问：

edit

        # v3.19: 从风格预设获取视频参数
        style_video_params = self._get_style_params(task_data, "video")

        # 记录用户动作

        # v3.19: 从风格预设获取视频参数
        style_video_params = self._get_style_params(task_data, "video")

        # v3.21: 镜头级视频参数辅助函数
        def _merge_shot_video_params(shot_data: dict) -> dict:
            """合并全局视频参数和镜头级覆盖"""
            merged = dict(style_video_params)
            if shot_data.get("shot_camera_type"):
                merged["camera_type"] = shot_data["shot_camera_type"]
            if shot_data.get("shot_transition_style"):
                merged["transition_style"] = shot_data["shot_transition_style"]
            return merged

        # 记录用户动作


Successfully replaced 1 block(s) in /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000/user_driven_task_system.py.

      ...
 1987
 1988         # v3.19: 从风格预设获取视频参数
 1989         style_video_params = self._get_style_params(task_data, "video")
 1990
-1991         # v3.21: 镜头级视频参数辅助函数
-1992         def _merge_shot_video_params(shot_data: dict) -> dict:
-1993             """合并全局视频参数和镜头级覆盖"""
-1994             merged = dict(style_video_params)
-1995             if shot_data.get("shot_camera_type"):
-1996                 merged["camera_type"] = shot_data["shot_camera_type"]
-1997             if shot_data.get("shot_transition_style"):
-1998                 merged["transition_style"] = shot_data["shot_transition_style"]
-1999             return merged
-2000
 2001         # 记录用户动作
 2002         task_data["user_actions"].append({
 2003             "action": "execute_produce",
 2004             "timestamp": datetime.now().isoformat(),
      ...


现在，在 `_get_style_params` 方法之后，将 `_merge_shot_video_params` 添加为一个类方法：


        collection = task_data.get("results", {}).get("collection", {})
        # collection可能是dict(有parsed字段)或str
        visual_style = None
        if isinstance(collection, dict):
            parsed = collection.get("parsed", {})
            visual_style = parsed.get("visual_style", {}) if isinstance(parsed, dict) else None

        if visual_style and isinstance(visual_style, dict):
            color_tone = visual_style.get("color_tone", "")

[3284 more lines in file. Use offset=740 to continue.]


让我找到 `_get_style_params` 方法的末尾：

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && awk '/^    def _get_style_params/,/^    def [a-zA-Z]/' user_driven_task_system.py | tail -20


shot_img = shot.get("image_path")
        if shot_img and Path(shot_img).exists():
            return shot_img

        # 2. 角色名匹配
        shot_desc = shot.get("description", "") + shot.get("video_prompt", "")
        for char in characters:
            char_name = char.get("name", "")
            char_img = char.get("image_path")
            if char_name and char_img and char_name in shot_desc and Path(char_img).exists():
                return char_img

        # 3. 场景背景图
        scene_img = scene.get("image_path")
        if scene_img and Path(scene_img).exists():
            return scene_img

        return None

    def execute_write_phase1(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:


我明白了，`_get_style_params` 方法在 `_resolve_shot_ref_image` 之前结束。让我找到确切的位置：

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "def _resolve_shot_ref_image\|def _get_style_params\|def execute_write" user_driven_task_system.py | head -10


651:    def _get_style_params(self, task_data: dict, param_type: str = "image") -> dict:
1235:    def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:
1259:    def execute_write_phase1(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:
1375:    def execute_write_phase2(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:
1558:    def execute_write_phase3(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:
1650:    def execute_write(self, task_id: str, user_input: Optional[Dict] = None) -> Dict[str, Any]:


        count = sum(1 for s in scenes for sh in s.get("shots",[]) if sh.get("image_path"))
        logger.info(f"  镜头参考图生成: {count}张(其中图生图{img2vid_count}张)")
        return scenes

    def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:
        """为镜头匹配最佳参考图
        优先级: 镜头参考图 > 角色匹配图 > 场景背景图 > None
        """
        # 1. 镜头自带的参考图(由image_prompt生成)

[2784 more lines in file. Use offset=1240 to continue.]


edit

        logger.info(f"  镜头参考图生成: {count}张(其中图生图{img2vid_count}张)")
        return scenes

    def _merge_shot_video_params(self, task_data: dict, shot_data: dict) -> dict:
        """v3.21: 合并全局视频参数和镜头级覆盖

        Args:
            task_data: 任务数据
            shot_data: 镜头数据(可能含shot_camera_type/shot_transition_style)
        Returns:
            dict: 合并后的视频参数
        """
        base_params = self._get_style_params(task_data, "video")
        merged = dict(base_params)
        if shot_data.get("shot_camera_type"):
            merged["camera_type"] = shot_data["shot_camera_type"]
        if shot_data.get("shot_transition_style"):
            merged["transition_style"] = shot_data["shot_transition_style"]
        return merged

    def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:

        logger.info(f"  镜头参考图生成: {count}张(其中图生图{img2vid_count}张)")
        return scenes

    def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:


      ...
 1231         count = sum(1 for s in scenes for sh in s.get("shots",[]) if sh.get("image_path"))
 1232         logger.info(f"  镜头参考图生成: {count}张(其中图生图{img2vid_count}张)")
 1233         return scenes
 1234
+1235     def _merge_shot_video_params(self, task_data: dict, shot_data: dict) -> dict:
+1236         """v3.21: 合并全局视频参数和镜头级覆盖
+1237
+1238         Args:
+1239             task_data: 任务数据
+1240             shot_data: 镜头数据(可能含shot_camera_type/shot_transition_style)
+1241         Returns:
+1242             dict: 合并后的视频参数
+1243         """
+1244         base_params = self._get_style_params(task_data, "video")
+1245         merged = dict(base_params)
+1246         if shot_data.get("shot_camera_type"):
+1247             merged["camera_type"] = shot_data["shot_camera_type"]
+1248         if shot_data.get("shot_transition_style"):
+1249             merged["transition_style"] = shot_data["shot_transition_style"]
+1250         return merged
+1251
 1235     def _resolve_shot_ref_image(self, shot: Dict, scene: Dict, characters: list) -> Optional[str]:
 1236         """为镜头匹配最佳参考图
 1237         优先级: 镜头参考图 > 角色匹配图 > 场景背景图 > None
 1238         """
      ...


现在更新所有调用点以使用 `self._merge_shot_video_params(task_data, shot)`:

exec

cd /root/.openclaw/workspace/versions/user_driven_final_v3.0.1/manhua_studio_web_8000 && grep -n "_merge_shot_video_params\|style_video_params" user_driven_task_system.py


1235:    def _merge_shot_video_params(self, task_data: dict, shot_data: dict) -> dict:
2006:        style_video_params = self._get_style_params(task_data, "video")
2078:                                    **_merge_shot_video_params(shot),
2767:                                    **_merge_shot_video_params(sh),
2776:                                    **_merge_shot_video_params(sh),
3284:                        **_merge_shot_video_params(original_shot or {}),


旧的局部函数调用需要更新为 `self._merge_shot_video_params(task_data, shot)`。

edit

                                    **self._merge_shot_video_params(task_data, sh),
                                )
                            else:
                                result = generate_video(
                                    prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **self._merge_shot_video_params(task_data, sh),

                                    **_merge_shot_video_params(sh),
                                )
                            else:
                                result = generate_video(
                                    prompt=vp,
                                    model=_video_model if _video_model and _video_model != "auto" else None,
                                    duration=sh.get("duration_seconds", 5),
                                    download_dir=str(videos_dir),
                                    # v3.21: 镜头级风格预设驱动视频参数
                                    **_merge_shot_video_params(sh),

**self._merge_shot_video_params(task_data, original_shot or {}),