diff --git a/apps/web/src/pages/ai-avatar/AiAvatar.css b/apps/web/src/pages/ai-avatar/AiAvatar.css index ba6bd51ad..8d19bbbac 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatar.css +++ b/apps/web/src/pages/ai-avatar/AiAvatar.css @@ -1,86 +1,71 @@ -/* AI 数字人页面样式 */ +/** + * AI数字人 — 页面样式(v3) + * 命名空间: .aa- (ai-avatar 缩写) + */ +/* ── 页面容器 ── */ .aa-page { - min-height: 100vh; - background: #f5f5f7; - padding: 20px; + display: flex; + flex-direction: column; + height: 100%; + min-width: 1280px; + background: #f5f6fa; + overflow-x: auto; } .aa-page-header { - margin-bottom: 20px; + padding: 16px 24px; + background: #fff; + border-bottom: 1px solid #e8e8ec; + flex-shrink: 0; } .aa-page-header h1 { - font-size: 24px; - font-weight: 600; - color: #1a1a2e; margin: 0; -} - -/* 步骤指示器 */ -.aa-steps { - display: flex; - align-items: center; - margin-bottom: 24px; - padding: 16px 20px; - background: #fff; - border-radius: 8px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); -} - -.aa-step { - display: flex; - align-items: center; - gap: 8px; -} - -.aa-step__number { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 50%; - background: #e8e8ed; - color: #8c8ca1; - font-size: 14px; + font-size: 20px; font-weight: 600; -} - -.aa-step.active .aa-step__number { - background: #6c5ce7; - color: #fff; -} - -.aa-step__label { - font-size: 14px; - color: #8c8ca1; -} - -.aa-step.active .aa-step__label { color: #1a1a2e; - font-weight: 500; } -.aa-step__connector { - flex: 1; - height: 2px; - background: #e8e8ed; - margin: 0 16px; -} - -/* 面板布局 */ .aa-page-body { + flex: 1; display: flex; - flex-direction: column; - gap: 16px; + gap: 12px; + padding: 16px; + overflow-x: auto; + min-height: 0; } +/* ── 面板通用 ── */ .aa-panel { background: #fff; - border-radius: 8px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); + border-radius: 12px; + border: 1px solid #e8e8ec; + display: flex; + flex-direction: column; overflow: hidden; + transition: width 0.2s ease; +} + +.aa-panel--p1 { + width: 20%; + min-width: 220px; +} +.aa-panel--p2 { + width: 20%; + min-width: 220px; +} +.aa-panel--p3 { + width: 25%; + min-width: 280px; +} +.aa-panel--p4 { + width: 17.5%; + min-width: 200px; +} +.aa-panel--p5 { + width: 17.5%; + min-width: 200px; } .aa-panel.collapsed .aa-panel__body { @@ -91,25 +76,34 @@ display: flex; align-items: center; justify-content: space-between; - padding: 14px 20px; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f5; cursor: pointer; user-select: none; - border-bottom: 1px solid #f0f0f0; + flex-shrink: 0; } .aa-panel__header:hover { - background: #fafafa; + background: #fafafe; } .aa-panel__title { - font-size: 15px; + font-size: 14px; font-weight: 600; color: #1a1a2e; } .aa-panel__toggle { - font-size: 12px; + width: 24px; + height: 24px; + border: none; + background: none; + cursor: pointer; color: #8c8ca1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; transition: transform 0.2s; } @@ -118,358 +112,385 @@ } .aa-panel__body { - padding: 20px; -} - -/* 步骤底部按钮 */ -.aa-step-actions { - display: flex; - justify-content: center; - padding: 20px 0; - gap: 12px; -} - -/* 按钮 */ -.aa-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 10px 24px; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - border: none; - transition: all 0.2s; -} - -.aa-btn--primary { - background: #6c5ce7; - color: #fff; -} - -.aa-btn--primary:hover { - background: #5b4cdb; -} - -.aa-btn--danger { - background: #ff4d4f; - color: #fff; -} - -.aa-btn--danger:hover { - background: #ff7875; -} - -.aa-btn:not(.aa-btn--primary):not(.aa-btn--danger) { - background: #f0f0f0; - color: #1a1a2e; -} - -.aa-btn:not(.aa-btn--primary):not(.aa-btn--danger):hover { - background: #e8e8ed; -} - -/* 弹窗 */ -.aa-modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.45); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.aa-modal { - background: #fff; - border-radius: 12px; - width: 480px; - max-width: 90vw; - max-height: 80vh; - overflow: hidden; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); -} - -.aa-modal__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 16px 20px; - border-bottom: 1px solid #f0f0f0; -} - -.aa-modal__title { - font-size: 16px; - font-weight: 600; - color: #1a1a2e; -} - -.aa-modal__close { - background: none; - border: none; - font-size: 18px; - cursor: pointer; - color: #8c8ca1; - padding: 4px; -} - -.aa-modal__close:hover { - color: #1a1a2e; -} - -.aa-modal__body { - padding: 20px; -} - -.aa-modal__footer { - display: flex; - justify-content: center; - padding: 16px 20px; - border-top: 1px solid #f0f0f0; -} - -/* 对口型 spinner */ -.aa-lipsync-spinner { - width: 48px; - height: 48px; - border: 4px solid #f0f0f0; - border-top-color: #6c5ce7; - border-radius: 50%; - animation: aa-spin 0.8s linear infinite; -} - -@keyframes aa-spin { - to { - transform: rotate(360deg); - } -} - -/* 文案列表 */ -.aa-script-list-header { - margin-bottom: 16px; -} - -.aa-input { - width: 100%; - padding: 10px 12px; - border: 1px solid #e8e8ed; - border-radius: 6px; - font-size: 14px; - outline: none; -} - -.aa-input:focus { - border-color: #6c5ce7; -} - -.aa-empty { - text-align: center; - padding: 40px 20px; - color: #8c8ca1; -} - -.aa-empty__icon { - font-size: 32px; - margin-bottom: 8px; -} - -.aa-script-list { - display: flex; - flex-direction: column; - gap: 8px; - max-height: 300px; - overflow-y: auto; -} - -.aa-script-item { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - border-radius: 6px; - cursor: pointer; - transition: background 0.2s; -} - -.aa-script-item:hover { - background: #f5f5f7; -} - -.aa-script-item__icon { - font-size: 20px; -} - -.aa-script-item__info { flex: 1; - min-width: 0; + overflow-y: auto; + padding: 16px; } -.aa-script-item__title { - font-size: 14px; - font-weight: 500; - color: #1a1a2e; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.aa-script-item__meta { +/* ── 通用表单元素 ── */ +.aa-label { + display: block; font-size: 12px; - color: #8c8ca1; - margin-top: 2px; + font-weight: 500; + color: #6b6b80; + margin-bottom: 6px; } -/* ── PanelScript 文案面板 ── */ -.aa-script-panel { - display: flex; - flex-direction: column; - gap: 8px; -} - -.aa-script-tabs { - display: flex; - gap: 8px; - margin-bottom: 4px; -} - -.aa-script-tab { - padding: 8px 16px; - border-radius: 6px; - border: 1px solid #e8e8ed; - background: #fff; +.aa-input, +.aa-select { + width: 100%; + padding: 8px 12px; + border: 1px solid #e0e0e8; + border-radius: 8px; font-size: 13px; - cursor: pointer; - transition: all 0.2s; + color: #1a1a2e; + background: #fff; + outline: none; + transition: border-color 0.15s; } -.aa-script-tab.active { - background: #6c5ce7; - color: #fff; - border-color: #6c5ce7; +.aa-input:focus, +.aa-select:focus { + border-color: #7c3aed; + box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.1); } .aa-textarea { width: 100%; - min-height: 120px; - padding: 12px; - border: 1px solid #e8e8ed; - border-radius: 6px; - font-size: 14px; - line-height: 1.6; - resize: vertical; + padding: 10px 12px; + border: 1px solid #e0e0e8; + border-radius: 8px; + font-size: 13px; + color: #1a1a2e; + background: #fff; outline: none; + resize: vertical; + min-height: 100px; font-family: inherit; + transition: border-color 0.15s; } .aa-textarea:focus { - border-color: #6c5ce7; + border-color: #7c3aed; + box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.1); } -.aa-char-count { - text-align: right; - font-size: 12px; - color: #8c8ca1; +.aa-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border: 1px solid #e0e0e8; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + background: #fff; + color: #1a1a2e; +} + +.aa-btn:hover { + background: #f5f5fa; + border-color: #c0c0d0; +} +.aa-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.aa-btn--primary { + background: #7c3aed; + color: #fff; + border-color: #7c3aed; +} +.aa-btn--primary:hover { + background: #6d28d9; } .aa-btn--ghost { background: transparent; - color: #6c5ce7; - border: 1px dashed #6c5ce7; + border-color: #d0d0dd; + color: #6b6b80; } -.aa-btn--ghost:hover { - background: #f0edff; +.aa-btn--danger { + background: #fee2e2; + border-color: #fca5a5; + color: #dc2626; +} +.aa-btn--danger:hover { + background: #fecaca; } .aa-btn--full { width: 100%; } -/* ── PanelLipsyncPreview 对口型预览面板 ── */ -.aa-lipsync-preview-panel { - display: flex; - flex-direction: column; - gap: 20px; -} - -.aa-lipsync-section { - display: flex; - flex-direction: column; - gap: 12px; -} - -.aa-lipsync-section__title { - font-size: 14px; +.aa-btn--generate { + background: linear-gradient(135deg, #7c3aed, #a855f7); + color: #fff; + border: none; + padding: 12px 20px; + font-size: 15px; font-weight: 600; - color: #1a1a2e; -} - -.aa-broll-badge { - font-size: 12px; - color: #6c5ce7; - background: #f0edff; - padding: 2px 8px; border-radius: 10px; } - -.aa-lipsync-actions { - display: flex; - flex-direction: column; - gap: 8px; +.aa-btn--generate:hover { + background: linear-gradient(135deg, #6d28d9, #9333ea); } -.aa-broll-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.aa-broll-item { +/* ── 竖屏视频预览 ── */ +.aa-video-preview { + width: 100%; + aspect-ratio: 9/16; + background: #0f0f1a; + border-radius: 10px; + overflow: hidden; display: flex; align-items: center; - gap: 12px; + justify-content: center; + position: relative; +} + +.aa-video-preview video { + width: 100%; + height: 100%; + object-fit: contain; +} + +.aa-video-preview__placeholder { + color: #6b6b80; + font-size: 13px; + text-align: center; +} + +/* ── 上传区域 ── */ +.aa-upload-zone { + border: 2px dashed #d0d0dd; + border-radius: 10px; + padding: 24px 16px; + text-align: center; + cursor: pointer; + transition: all 0.15s; + background: #fafafe; +} + +.aa-upload-zone:hover { + border-color: #7c3aed; + background: #f5f0ff; +} + +.aa-upload-zone__icon { + font-size: 28px; + margin-bottom: 8px; + color: #8c8ca1; +} + +.aa-upload-zone__text { + font-size: 13px; + color: #6b6b80; +} + +/* ── 视频信息卡片 ── */ +.aa-video-info { + margin-top: 10px; padding: 10px 12px; - background: #f9f9fb; - border-radius: 6px; + background: #f8f8fc; + border-radius: 8px; + font-size: 12px; + color: #6b6b80; } -.aa-broll-item__thumb { - width: 64px; - height: 36px; - object-fit: cover; - border-radius: 4px; - background: #e8e8ed; +.aa-video-info__row { + display: flex; + justify-content: space-between; + padding: 3px 0; } -.aa-broll-item__info { +/* ── 配音面板 ── */ +.aa-voice-source-toggle { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.aa-voice-source-btn { + flex: 1; + padding: 8px; + border: 1px solid #e0e0e8; + border-radius: 8px; + font-size: 12px; + font-weight: 500; + text-align: center; + cursor: pointer; + background: #fff; + color: #6b6b80; + transition: all 0.15s; +} + +.aa-voice-source-btn.active { + background: #f5f0ff; + border-color: #7c3aed; + color: #7c3aed; +} + +.aa-voice-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 240px; + overflow-y: auto; +} + +.aa-voice-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid #e8e8ec; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; +} + +.aa-voice-card:hover { + border-color: #c0c0d0; + background: #fafafe; +} + +.aa-voice-card.selected { + border-color: #7c3aed; + background: #f5f0ff; +} + +.aa-voice-card__radio { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid #d0d0dd; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.aa-voice-card.selected .aa-voice-card__radio { + border-color: #7c3aed; +} + +.aa-voice-card.selected .aa-voice-card__radio::after { + content: ""; + width: 8px; + height: 8px; + border-radius: 50%; + background: #7c3aed; +} + +.aa-voice-card__info { flex: 1; min-width: 0; } -.aa-broll-item__remove { - background: none; - border: none; - color: #8c8ca1; - cursor: pointer; - font-size: 14px; - padding: 4px; +.aa-voice-card__name { + font-size: 13px; + font-weight: 500; + color: #1a1a2e; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.aa-broll-item__remove:hover { - color: #ff4d4f; +.aa-voice-card__desc { + font-size: 11px; + color: #8c8ca1; + margin-top: 2px; +} + +.aa-voice-card__preview { + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background: #f0f0f5; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + flex-shrink: 0; +} + +.aa-voice-card__preview:hover { + background: #e0e0ea; +} + +/* ── 配音参数 ── */ +.aa-voice-params { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f5; + display: flex; + flex-direction: column; + gap: 12px; +} + +.aa-voice-params__row { + display: flex; + gap: 8px; +} + +.aa-voice-params__field { + flex: 1; +} + +.aa-clone-entry { + margin-top: 12px; + text-align: center; +} + +/* ── 文案 & 对口型 ── */ +.aa-script-tabs { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.aa-script-tab { + padding: 6px 14px; + border: 1px solid #e0e0e8; + border-radius: 6px; + font-size: 12px; + cursor: pointer; + background: #fff; + color: #6b6b80; + transition: all 0.15s; +} + +.aa-script-tab.active { + background: #7c3aed; + border-color: #7c3aed; + color: #fff; +} + +.aa-char-count { + text-align: right; + font-size: 11px; + color: #8c8ca1; + margin-top: 4px; +} + +.aa-lipsync-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f5; +} + +.aa-lipsync-section__title { + font-size: 13px; + font-weight: 600; + color: #1a1a2e; + margin-bottom: 12px; } .aa-lipsync-preview { width: 100%; - aspect-ratio: 9 / 16; - max-height: 400px; - background: #1a1a2e; - border-radius: 8px; + aspect-ratio: 9/16; + background: #0f0f1a; + border-radius: 10px; overflow: hidden; + margin-bottom: 12px; display: flex; align-items: center; justify-content: center; @@ -481,22 +502,747 @@ object-fit: contain; } -.aa-video-preview__placeholder { - text-align: center; - color: #8c8ca1; - font-size: 14px; +.aa-lipsync-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; } +.aa-broll-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: #f0fdf4; + border: 1px solid #86efac; + border-radius: 12px; + font-size: 11px; + color: #16a34a; +} + +/* ── 标题配置 ── */ +.aa-title-input { + margin-bottom: 16px; +} + +.aa-title-style-section { + margin-top: 12px; +} + +.aa-subtitle-toggle { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f5; +} + +.aa-checkbox-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: #1a1a2e; + cursor: pointer; +} + +/* ── 封面 & 生成 ── */ +.aa-cover-preview { + width: 100%; + aspect-ratio: 9/16; + max-height: 160px; + background: #f0f0f5; + border-radius: 10px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 12px; +} + +.aa-cover-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.aa-cover-preview__placeholder { + font-size: 12px; + color: #8c8ca1; +} + +.aa-cover-actions { + display: flex; + gap: 8px; + margin-bottom: 16px; +} + +.aa-generate-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f5; +} + +.aa-config-summary { + margin: 12px 0; + padding: 12px; + background: #f8f8fc; + border-radius: 8px; + font-size: 12px; +} + +.aa-config-summary__row { + display: flex; + justify-content: space-between; + padding: 4px 0; + color: #6b6b80; +} + +.aa-config-summary__value { + color: #1a1a2e; + font-weight: 500; +} + +.aa-config-summary__empty { + color: #c0c0d0; + font-style: italic; +} + +/* ── 状态标签 ── */ +.aa-status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.aa-status-badge--idle { + background: #f0f0f5; + color: #8c8ca1; +} +.aa-status-badge--pending { + background: #fef3c7; + color: #d97706; +} +.aa-status-badge--processing { + background: #dbeafe; + color: #2563eb; +} +.aa-status-badge--completed { + background: #d1fae5; + color: #059669; +} +.aa-status-badge--failed { + background: #fee2e2; + color: #dc2626; +} + +/* ── 进度条 ── */ .aa-progress { width: 100%; height: 6px; - background: #333; + background: #e8e8ec; border-radius: 3px; overflow: hidden; + margin: 8px 0; } .aa-progress__bar { height: 100%; - background: #6c5ce7; + background: linear-gradient(90deg, #7c3aed, #a855f7); + border-radius: 3px; transition: width 0.3s; } + +/* ── 弹窗通用 ── */ +.aa-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.aa-modal { + background: #fff; + border-radius: 14px; + width: 90%; + max-width: 720px; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15); +} + +.aa-modal__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid #f0f0f5; +} + +.aa-modal__title { + font-size: 16px; + font-weight: 600; + color: #1a1a2e; +} + +.aa-modal__close { + width: 32px; + height: 32px; + border: none; + background: none; + cursor: pointer; + font-size: 18px; + color: #8c8ca1; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; +} + +.aa-modal__close:hover { + background: #f0f0f5; +} + +.aa-modal__body { + flex: 1; + overflow-y: auto; + padding: 20px; +} + +.aa-modal__footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 20px; + border-top: 1px solid #f0f0f5; +} + +/* ── 素材库弹窗 ── */ +.aa-asset-search { + display: flex; + gap: 10px; + margin-bottom: 16px; +} + +.aa-asset-search .aa-input { + flex: 1; +} + +.aa-asset-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; +} + +.aa-asset-card { + aspect-ratio: 9/16; + border-radius: 8px; + overflow: hidden; + border: 2px solid transparent; + cursor: pointer; + position: relative; + background: #f0f0f5; + transition: border-color 0.15s; +} + +.aa-asset-card:hover { + border-color: #c0c0d0; +} +.aa-asset-card.selected { + border-color: #7c3aed; +} + +.aa-asset-card img, +.aa-asset-card video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.aa-asset-card__name { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 4px 6px; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); + color: #fff; + font-size: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.aa-asset-card__check { + position: absolute; + top: 6px; + right: 6px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #7c3aed; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +/* ── B-roll 弹窗 ── */ +.aa-broll-modal-body { + display: flex; + gap: 20px; +} + +.aa-broll-left { + flex: 1; +} + +.aa-broll-right { + width: 220px; + flex-shrink: 0; +} + +.aa-broll-asset-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-bottom: 16px; +} + +.aa-broll-asset-thumb { + aspect-ratio: 9/16; + border-radius: 6px; + overflow: hidden; + border: 2px solid transparent; + cursor: pointer; + position: relative; + background: #f0f0f5; +} + +.aa-broll-asset-thumb.selected { + border-color: #7c3aed; +} + +.aa-broll-asset-thumb.already-selected { + opacity: 0.4; + pointer-events: none; +} + +.aa-broll-asset-thumb.already-selected::after { + content: "已选择"; + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 11px; + display: flex; + align-items: center; + justify-content: center; +} + +.aa-broll-asset-thumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.aa-broll-settings { + display: flex; + flex-direction: column; + gap: 12px; +} + +.aa-broll-mode-toggle { + display: flex; + gap: 6px; +} + +.aa-broll-mode-btn { + flex: 1; + padding: 6px 10px; + border: 1px solid #e0e0e8; + border-radius: 6px; + font-size: 11px; + text-align: center; + cursor: pointer; + background: #fff; + color: #6b6b80; +} + +.aa-broll-mode-btn.active { + background: #f5f0ff; + border-color: #7c3aed; + color: #7c3aed; +} + +.aa-pip-positions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} + +.aa-pip-pos-btn { + padding: 6px; + border: 1px solid #e0e0e8; + border-radius: 6px; + font-size: 11px; + text-align: center; + cursor: pointer; + background: #fff; + color: #6b6b80; +} + +.aa-pip-pos-btn.active { + background: #f5f0ff; + border-color: #7c3aed; + color: #7c3aed; +} + +/* ── 已配置 B-roll 列表 ── */ +.aa-broll-list { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.aa-broll-list__title { + font-size: 12px; + font-weight: 600; + color: #6b6b80; +} + +.aa-broll-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: #f8f8fc; + border-radius: 8px; + font-size: 12px; +} + +.aa-broll-item__thumb { + width: 32px; + height: 32px; + border-radius: 4px; + object-fit: cover; + background: #e8e8ec; +} + +.aa-broll-item__info { + flex: 1; +} + +.aa-broll-item__remove { + width: 24px; + height: 24px; + border: none; + background: none; + cursor: pointer; + color: #dc2626; + font-size: 14px; +} + +/* ── 文案列表(弹窗内) ── */ +.aa-script-list-header { + display: flex; + gap: 10px; + margin-bottom: 16px; +} + +.aa-script-list-header .aa-input { + flex: 1; +} + +.aa-script-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.aa-script-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border: 1px solid #e8e8ec; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s; +} + +.aa-script-item:hover { + border-color: #c0c0d0; + background: #fafafe; +} +.aa-script-item.selected { + border-color: #7c3aed; + background: #f5f0ff; +} + +.aa-script-item__icon { + font-size: 20px; + color: #8c8ca1; +} + +.aa-script-item__info { + flex: 1; +} + +.aa-script-item__title { + font-size: 13px; + font-weight: 500; + color: #1a1a2e; +} + +.aa-script-item__meta { + font-size: 11px; + color: #8c8ca1; + margin-top: 2px; +} + +.aa-empty { + text-align: center; + padding: 32px 16px; + color: #8c8ca1; + font-size: 13px; +} + +.aa-empty__icon { + font-size: 36px; + margin-bottom: 8px; +} + +/* ============================================================ + #1809 ④⑤⑥ B-roll 弹窗:选库行 + 文案句子列表 + 自动估算提示 + ============================================================ */ + +/* 宽弹窗:左右两栏 + 句子列表需要更大空间 */ +.aa-modal--wide { + max-width: 960px; + width: 94%; +} + +.aa-broll-modal-body .aa-broll-right { + width: 300px; +} + +/* 左侧素材库选择行 */ +.aa-broll-lib-row { + margin-bottom: 10px; +} + +.aa-broll-lib-row .aa-select { + width: 100%; + height: 36px; + border: 1px solid var(--border-color, #e2e2ea); + border-radius: 8px; + padding: 0 10px; + font-size: 13px; + background: #fff; + color: #1a1a2e; + outline: none; +} + +/* 无缩略图时的素材占位 */ +.aa-broll-asset-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; +} + +/* 文案句子列表 */ +.aa-sentence-list { + max-height: 240px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + padding-right: 4px; +} + +.aa-sentence-item { + display: flex; + align-items: flex-start; + gap: 8px; + width: 100%; + text-align: left; + border: 1px solid var(--border-color, #e2e2ea); + border-radius: 8px; + background: #fff; + padding: 8px 10px; + cursor: pointer; + transition: all 0.15s; +} + +.aa-sentence-item:hover { + border-color: #c0c0d0; + background: #f8f8fc; +} + +.aa-sentence-item.active { + border-color: #7c3aed; + background: #f3edff; +} + +.aa-sentence-item__idx { + flex-shrink: 0; + width: 20px; + height: 20px; + border-radius: 50%; + background: #f0f0f5; + color: #6b6b80; + font-size: 11px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} + +.aa-sentence-item.active .aa-sentence-item__idx { + background: #7c3aed; + color: #fff; +} + +.aa-sentence-item__text { + flex: 1; + font-size: 12px; + line-height: 1.5; + color: #1a1a2e; + word-break: break-all; +} + +.aa-sentence-item__time { + flex-shrink: 0; + font-size: 10px; + color: #8c8ca1; + margin-top: 2px; +} + +.aa-sentence-empty { + font-size: 12px; + color: #8c8ca1; + background: #f8f8fc; + border-radius: 8px; + padding: 10px; +} + +/* 选择/估算提示 */ +.aa-broll-hint { + font-size: 12px; + color: #059669; + background: #f8f8fc; + border-radius: 6px; + padding: 8px 10px; + display: flex; + flex-direction: column; + gap: 4px; +} + +/* ─ 对口型生成弹窗 Spinner ── */ +.aa-lipsync-spinner { + width: 48px; + height: 48px; + border: 4px solid #f0f0f5; + border-top-color: #6366f1; + border-radius: 50%; + animation: aa-spin 0.8s linear infinite; +} + +@keyframes aa-spin { + to { + transform: rotate(360deg); + } +} + +.aa-btn--danger { + background: #ff4d4f; + color: #fff; + border: none; +} + +.aa-btn--danger:hover { + background: #ff7875; +} + +/* ── v3.1 两步骤导航(仅追加,不改动上方任何原有样式) ── */ +.aa-step-nav { + display: flex; + align-items: center; + gap: 24px; + padding: 10px 24px; + background: #fff; + border-bottom: 1px solid #e8e8ec; + flex-shrink: 0; +} + +.aa-step-nav__item { + font-size: 13px; + font-weight: 500; + color: #8c8ca1; + position: relative; + padding-bottom: 6px; +} + +.aa-step-nav__item.active { + color: #4f46e5; + font-weight: 600; +} + +.aa-step-nav__item.active::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -11px; + height: 2px; + background: #4f46e5; + border-radius: 1px; +} + +/* 步骤切换按钮行 */ +.aa-step-btn-row { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + padding-top: 12px; + border-top: 1px dashed #f0f0f5; +} + +.aa-step-btn-row .aa-btn { + min-width: 120px; +} + +/* 分步布局:每个步骤 3 个横向面板等宽撑满(沿用原 .aa-panel 外观,仅改宽度) */ +.aa-page-body > .aa-panel--s1, +.aa-page-body > .aa-panel--s2 { + flex: 1 1 0; + width: auto; + min-width: 220px; +} + +/* 文案 / 对口型预览面板内容较多,给更宽的弹性比例 */ +.aa-page-body > .aa-panel--s1-wide { + flex: 1.4 1 0; + width: auto; + min-width: 280px; +} + +.aa-page-body > .aa-panel--s2-wide { + flex: 1.4 1 0; + width: auto; + min-width: 300px; +} diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index 06bf75464..0c5d807ee 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -1,7 +1,7 @@ /** - * AI数字人 — 主页面(两步骤版) + * AI数字人 — 主页面(v3 两步骤版) * 步骤1:出镜视频 / 配音库 / 文案 - * 步骤2:插入画面 / 对口型预览 / 标题配置 / 封面&生成 + * 步骤2:对口型预览(含插入画面)/ 标题配置 / 封面&生成 */ import React, { useState, useCallback, useEffect, useRef } from "react" import { message } from "antd" @@ -30,17 +30,11 @@ import { } from "./utils/contract" /** 面板折叠状态 */ -type PanelKey = - | "video" - | "voice" - | "script" - | "lipsync" - | "title" - | "cover" +type PanelKey = "video" | "voice" | "script" | "lipsync" | "title" | "cover" const AiAvatarPage: React.FC = () => { const state = useAiAvatar() - const [currentStep, setCurrentStep] = useState(1) + const [currentStep, setCurrentStep] = useState<1 | 2>(1) const [collapsed, setCollapsed] = useState>({ video: false, voice: false, @@ -52,11 +46,11 @@ const AiAvatarPage: React.FC = () => { /* ── 对口型生成弹窗 ── */ const [showLipsyncModal, setShowLipsyncModal] = useState(false) - const [lipsyncStatus, setLipsyncStatus] = useState< - "generating" | "completed" | "failed" - >("generating") + const [lipsyncStatus, setLipsyncStatus] = useState<"generating" | "completed" | "failed">( + "generating", + ) const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") - /* ─ 智能封面加载态 ── */ + /* ── 智能封面加载态 ── */ const [smartCoverLoading, setSmartCoverLoading] = useState(false) /* ── 对口型轮询 ── */ @@ -67,24 +61,25 @@ const AiAvatarPage: React.FC = () => { }, []) /* ── 步骤切换 ── */ - const canGoToStep2 = useCallback(() => { - return state.selectedVideo && state.selectedVoice && state.scriptText.trim() - }, [state.selectedVideo, state.selectedVoice, state.scriptText]) - const handleNextStep = useCallback(() => { - if (!canGoToStep2()) { - message.warning("请先完成出镜视频、配音和文案的选择") + const missing: string[] = [] + if (!state.selectedVideo) missing.push("出镜视频") + if (!state.selectedVoice) missing.push("配音") + if (!state.scriptText.trim()) missing.push("文案") + if (missing.length > 0) { + message.warning(`请先完成${missing.join("、")}`) return } setCurrentStep(2) - }, [canGoToStep2]) + }, [state.selectedVideo, state.selectedVoice, state.scriptText]) const handlePrevStep = useCallback(() => { setCurrentStep(1) }, []) - /* ── 对口型 ─ */ + /* ── 对口型 ── */ const handleGenerateLipsync = useCallback(async () => { + // ② 缺项明确提示(#1809):不再静默 return const video = state.selectedVideo const voice = state.selectedVoice const text = state.scriptText.trim() @@ -97,10 +92,12 @@ const AiAvatarPage: React.FC = () => { return } try { + // 显示生成弹窗 setShowLipsyncModal(true) setLipsyncStatus("generating") setLipsyncErrorMessage("") + // ① 先按素材 id 拿 file_url(#1809 补充:对齐后端新参数 video_url) console.log("[对口型] 开始生成:", { videoId: video.id, voiceId: voice.voice_id, @@ -119,20 +116,19 @@ const AiAvatarPage: React.FC = () => { message.error("获取出镜视频播放地址失败,请重新选择素材") return } + // ② 模式A TTS直生:video_url + voice_id + script_text,语速/情绪英文枚举透传(#1822) const payload = { voice_id: voice.voice_id, script_text: state.scriptText, video_url: videoUrl, - speed: state.speed, - emotion: normalizeEmotion(state.emotion), + speed: state.speed, // 语速 0.5~2.0 + emotion: normalizeEmotion(state.emotion), // natural/excited/calm/friendly } console.log("[对口型] createLipsyncJob 请求:", payload) const job = await createLipsyncJob(payload) - console.log("[对口型] createLipsyncJob 响应:", { - id: job.id, - status: job.status, - }) + console.log("[对口型] createLipsyncJob 响应:", { id: job.id, status: job.status }) state.setLipsyncJob(job) + // 开始轮询 if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) lipsyncTimerRef.current = setInterval(async () => { try { @@ -153,9 +149,7 @@ const AiAvatarPage: React.FC = () => { } else if (updated.status === "failed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) setLipsyncStatus("failed") - setLipsyncErrorMessage( - updated.error_message || "对口型生成失败", - ) + setLipsyncErrorMessage(updated.error_message || "对口型生成失败") } } catch (err) { console.error("[对口型] 轮询错误:", err) @@ -168,19 +162,12 @@ const AiAvatarPage: React.FC = () => { message: err instanceof Error ? err.message : String(err), }) setShowLipsyncModal(false) - message.error( - err instanceof Error ? err.message : "对口型任务提交失败,请重试", - ) + message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - state.selectedVideo, - state.selectedVoice, - state.scriptText, - state.speed, - state.emotion, - ]) + }, [state.selectedVideo, state.selectedVoice, state.scriptText, state.speed, state.emotion]) + // 取消对口型生成 const handleCancelLipsync = useCallback(() => { if (lipsyncTimerRef.current) { clearInterval(lipsyncTimerRef.current) @@ -191,6 +178,7 @@ const AiAvatarPage: React.FC = () => { setLipsyncErrorMessage("") }, []) + // 清理轮询 useEffect(() => { return () => { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) @@ -199,6 +187,7 @@ const AiAvatarPage: React.FC = () => { /* ── 生成视频 ── */ const handleGenerate = useCallback(async () => { + // ② 前置条件提示(#1809) if (!state.lipsyncJob || state.lipsyncJob.status !== "completed") { message.warning("请先生成对口型视频,待对口型完成后再提交渲染") return @@ -209,19 +198,16 @@ const AiAvatarPage: React.FC = () => { lipsync_job_id: state.lipsyncJob.id, script_id: state.script?.id, b_roll_segments: state.bRollSegments as never, + // 字段映射:build_title_drawtext_filter 真实口径 text/font_size/font_color/position/... title_config: buildTitleConfigPayload(state.titleConfig), - cover_config: buildCoverConfigPayload( - state.coverConfig, - state.coverConfig.smart_cover_url, - ), + // cover_config:智能封面 cover_url + 截帧 timestamp + cover_config: buildCoverConfigPayload(state.coverConfig, state.coverConfig.smart_cover_url), resolution: state.resolution, }) message.success("渲染任务已提交,可在视频管理中查看进度") } catch (err) { console.error("渲染任务提交失败:", err) - message.error( - err instanceof Error ? err.message : "渲染任务提交失败,请重试", - ) + message.error(err instanceof Error ? err.message : "渲染任务提交失败,请重试") } finally { state.setIsGenerating(false) } @@ -235,8 +221,9 @@ const AiAvatarPage: React.FC = () => { state.resolution, ]) - /* ── 智能封面 ── */ + /* ── 智能封面:调后端 MediaKit 选帧接口(#1822) ── */ const handleSmartCover = useCallback(async () => { + // 基于对口型成片抽帧,必须先完成对口型 const videoUrl = state.lipsyncJob?.output_video_url if (state.lipsyncJob?.status !== "completed" || !videoUrl) { message.warning("请先生成对口型视频,完成后再智能获取封面") @@ -258,9 +245,7 @@ const AiAvatarPage: React.FC = () => { } } catch (err) { console.error("智能封面生成失败:", err) - message.error( - err instanceof Error ? err.message : "智能封面生成失败,请重试", - ) + message.error(err instanceof Error ? err.message : "智能封面生成失败,请重试") } finally { setSmartCoverLoading(false) } @@ -281,36 +266,26 @@ const AiAvatarPage: React.FC = () => { return (
-

AI 数字人

+

AI数字人

- {/* 步骤指示器 */} -
-
= 1 ? "active" : ""}`}> - 1 - 视频 / 配音 / 文案 -
-
-
= 2 ? "active" : ""}`}> - 2 - - 对口型 / 标题 / 封面 / 生成 - -
+ {/* 步骤切换导航条 */} +
+ + 1. 视频 / 配音 / 文案 + + + 2. 对口型 / 标题 / 封面 / 生成 +
- {/* ════ 步骤 1:出镜视频 / 配音库 / 文案 ═══ */} + {/* ════ 步骤 1:出镜视频 / 配音库 / 文案 ════ */} {currentStep === 1 && ( <> - {/* 面板 1:出镜视频 */} -
-
togglePanel("video")} - > + {/* 面板1:出镜视频 */} +
+
togglePanel("video")}> 出镜视频 ▼
@@ -324,14 +299,9 @@ const AiAvatarPage: React.FC = () => {
- {/* 面板 2:配音库 */} -
-
togglePanel("voice")} - > + {/* 面板2:配音库 */} +
+
togglePanel("voice")}> 配音库 ▼
@@ -351,15 +321,10 @@ const AiAvatarPage: React.FC = () => {
- {/* 面板 3:文案 */} -
-
togglePanel("script")} - > - 文案 + {/* 面板3:文案 */} +
+
togglePanel("script")}> + 文案 & 对口型 ▼
@@ -368,35 +333,23 @@ const AiAvatarPage: React.FC = () => { onScriptTextChange={state.setScriptText} onOpenScriptModal={() => state.setShowScriptModal(true)} /> +
+ +
- - {/* 步骤 1 底部按钮 */} -
- -
)} - {/* ════ 步骤 2:插入画面 / 对口型预览 / 标题配置 / 封面&生成 ═══ */} + {/* ════ 步骤 2:对口型预览(含插入画面)/ 标题配置 / 封面&生成 ════ */} {currentStep === 2 && ( <> - {/* 面板 4:插入画面 & 对口型预览 */} -
-
togglePanel("lipsync")} - > - - 对口型预览 - + {/* 面板:对口型预览 + 插入画面 */} +
+
togglePanel("lipsync")}> + 对口型预览 ▼
@@ -407,36 +360,28 @@ const AiAvatarPage: React.FC = () => { onOpenBRollModal={() => state.setShowBRollModal(true)} onRemoveBRoll={state.removeBRollSegment} /> +
+ +
- {/* 面板 5:标题配置 */} -
-
togglePanel("title")} - > + {/* 面板4:标题配置 */} +
+
togglePanel("title")}> 标题配置 ▼
- +
- {/* 面板 6:封面 & 生成 */} -
-
togglePanel("cover")} - > + {/* 面板5:封面 & 生成 */} +
+
togglePanel("cover")}> 封面 & 生成 ▼
@@ -444,16 +389,11 @@ const AiAvatarPage: React.FC = () => { - state.setCoverConfig((prev) => ({ - ...prev, - ...partial, - })) + state.setCoverConfig((prev) => ({ ...prev, ...partial })) } onSmartCover={handleSmartCover} smartCoverLoading={smartCoverLoading} - canSmartCover={ - state.lipsyncJob?.status === "completed" - } + canSmartCover={state.lipsyncJob?.status === "completed"} resolution={state.resolution} onResolutionChange={state.setResolution} isGenerating={state.isGenerating} @@ -462,13 +402,6 @@ const AiAvatarPage: React.FC = () => { />
- - {/* 步骤 2 底部按钮 */} -
- -
)}
@@ -511,10 +444,7 @@ const AiAvatarPage: React.FC = () => {
e.stopPropagation()}>
对口型生成 -
@@ -530,22 +460,10 @@ const AiAvatarPage: React.FC = () => { {lipsyncStatus === "generating" && ( <>
-
+
对口型视频生成中…
-
+
请勿关闭页面,完成后将自动提示
@@ -553,13 +471,7 @@ const AiAvatarPage: React.FC = () => { {lipsyncStatus === "completed" && ( <>
✅
-
+
对口型视频生成完成
@@ -567,23 +479,11 @@ const AiAvatarPage: React.FC = () => { {lipsyncStatus === "failed" && ( <>
❌
-
+
对口型生成失败
{lipsyncErrorMessage && ( -
+
{lipsyncErrorMessage}
)} @@ -592,10 +492,7 @@ const AiAvatarPage: React.FC = () => {
{lipsyncStatus === "generating" && ( - )} @@ -663,17 +560,12 @@ const ScriptSelectModalLazy: React.FC<{ ) : (
{filtered.map((s) => ( -
onSelect(s)} - > +
onSelect(s)}> 📄
{s.title}
- {s.char_count}字 ·{" "} - {new Date(s.created_at).toLocaleDateString()} + {s.char_count}字 · {new Date(s.created_at).toLocaleDateString()}
diff --git a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx index 1e32c5186..2b40a4380 100755 --- a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx @@ -43,7 +43,7 @@ export function PanelLipsyncPreview({ : "对口型生成中…" return ( -
+
{/* ── B-roll 画面 ── */}
diff --git a/apps/web/src/pages/ai-avatar/components/PanelScript.tsx b/apps/web/src/pages/ai-avatar/components/PanelScript.tsx index 7ff8b91d7..87d28114a 100755 --- a/apps/web/src/pages/ai-avatar/components/PanelScript.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelScript.tsx @@ -20,7 +20,7 @@ export function PanelScript({ const [scriptTab, setScriptTab] = useState("library") return ( -
+
{/* ── Tab 切换 ── */}