| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- <section class="dh-page pl-page t2v-page">
- <!-- 创作会话顶栏:新建 / 历史 / 状态徽章 -->
- <app-session-bar
- pipelineId="topic-to-video"
- pipelineLabel="主题生视频"
- (newSession)="onNewSession()"
- (openSession)="onOpenSession($event)">
- </app-session-bar>
- <header class="dh-page-header">
- <div class="dh-header-icon">
- <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <circle cx="12" cy="12" r="9"></circle>
- <path d="M12 7v5l3 2"></path>
- </svg>
- </div>
- <div class="dh-header-text">
- <h2>主题生视频 <span class="pl-pill">快速 / 手动</span></h2>
- <p>快速:一句话主题 → AI 拆分镜 → 配图 → 可选动态视频 → 拼接成片。手动:逐卡填 prompt、上传或 AI 生成首尾帧、单段图生视频,最后一键拼接。</p>
- </div>
- </header>
- <div class="t2v-template-panel">
- <div class="t2v-template-panel__main">
- <div class="t2v-template-panel__title">模板/预设</div>
- <div class="t2v-template-panel__controls">
- <select [(ngModel)]="selectedTemplateId" [disabled]="isRunning || hasInflightScene">
- <option value="">选择主题生视频模板</option>
- <option *ngFor="let template of topicTemplates" [value]="template.id">
- {{ template.scope === 'system' ? '系统' : '我的' }} · {{ template.name }}
- </option>
- </select>
- <button class="dh-chip-btn" type="button" [disabled]="!selectedTemplateId || isRunning || hasInflightScene" (click)="applySelectedTemplate()">
- 应用模板
- </button>
- </div>
- <p class="t2v-template-panel__message" *ngIf="templateMessage">{{ templateMessage }}</p>
- </div>
- <div class="t2v-template-panel__save">
- <input class="dh-input" type="text" placeholder="模板名称(可选)" [(ngModel)]="templateName" [disabled]="isRunning || hasInflightScene">
- <button class="dh-chip-btn" type="button" [disabled]="isRunning || hasInflightScene" (click)="saveCurrentAsTemplate()">
- 保存为模板
- </button>
- </div>
- </div>
- <!-- 顶部模式切换条(独立横向,使两种模式视觉分明) -->
- <div class="t2v-mode-bar">
- <button class="t2v-mode-bar__btn"
- [class.is-active]="quickMode"
- [disabled]="isRunning" (click)="quickMode = true">
- <span class="t2v-mode-bar__title">⚡ 快速模式</span>
- <span class="t2v-mode-bar__sub">一句主题 → AI 全自动出片</span>
- </button>
- <button class="t2v-mode-bar__btn"
- [class.is-active]="!quickMode"
- [disabled]="isRunning" (click)="quickMode = false">
- <span class="t2v-mode-bar__title">✍️ 手动模式</span>
- <span class="t2v-mode-bar__sub">逐卡填 prompt · 支持上传 / 首尾帧 i2v</span>
- </button>
- </div>
- <div class="pl-grid">
- <!-- ========================= 左:配置 ========================= -->
- <div class="dh-step-col">
- <div class="dh-step-col__head">
- <span class="dh-step-col__step">Step 1</span>
- <span class="dh-step-col__title">{{ quickMode ? '主题与配置' : '通用设置' }}</span>
- </div>
- <!-- 主题:快速模式必填且置顶;手动模式收为可选标题 -->
- <div class="dh-substep" *ngIf="quickMode">
- <div class="dh-substep__head">
- <h4>视频主题 <em>*</em></h4>
- </div>
- <textarea class="dh-textarea" rows="3" maxlength="120"
- placeholder="例如:探索中国西部最美高原 / 一杯美式拯救加班的下午 / 用 30 秒讲清楚什么是大语言模型"
- [(ngModel)]="topic" (ngModelChange)="syncDraft()" [disabled]="isRunning"></textarea>
- <div class="dh-field__counter">{{ topic.length }} / 120 字</div>
- </div>
- <!-- 分镜数量:仅快速模式 -->
- <div class="dh-substep" *ngIf="quickMode">
- <div class="dh-substep__head"><h4>分镜数量</h4></div>
- <div class="dh-segment">
- <button class="dh-segment__item" [class.is-active]="nScenes === 3" [disabled]="isRunning" (click)="nScenes = 3">3 段</button>
- <button class="dh-segment__item" [class.is-active]="nScenes === 5" [disabled]="isRunning" (click)="nScenes = 5">5 段</button>
- <button class="dh-segment__item" [class.is-active]="nScenes === 6" [disabled]="isRunning" (click)="nScenes = 6">6 段</button>
- <button class="dh-segment__item" [class.is-active]="nScenes === 8" [disabled]="isRunning" (click)="nScenes = 8">8 段</button>
- </div>
- <p class="t2v-tip">{{ nScenes }} 段约 {{ nScenes * 6 }}-{{ nScenes * 8 }} 秒。</p>
- </div>
- <!-- 视觉风格:两模式共用 -->
- <div class="dh-substep">
- <div class="dh-substep__head"><h4>视觉风格</h4></div>
- <div class="dh-segment dh-segment--wrap">
- <button class="dh-segment__item" *ngFor="let s of stylePresets"
- [class.is-active]="style === s.value"
- [disabled]="isRunning" (click)="style = s.value">
- {{ s.label }}
- </button>
- </div>
- </div>
- <!-- 画面比例:两模式共用 -->
- <div class="dh-substep">
- <div class="dh-substep__head"><h4>画面比例</h4></div>
- <div class="dh-segment">
- <button class="dh-segment__item" *ngFor="let a of aspectPresets"
- [class.is-active]="aspect === a.value"
- [disabled]="isRunning" (click)="aspect = a.value">
- {{ a.label }}
- </button>
- </div>
- </div>
- <div class="dh-substep">
- <div class="dh-substep__head">
- <h4>全局一致性设定</h4>
- <span class="t2v-tip">作为默认锚点;每个分镜仍可自由改动作和构图</span>
- </div>
- <label class="t2v-toggle-row">
- <input type="checkbox" [(ngModel)]="continuityEnabled" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <span>启用角色 / 场景 / 产品一致性增强</span>
- </label>
- <div class="t2v-bible-grid" *ngIf="continuityEnabled">
- <input class="dh-input" type="text" placeholder="主体/角色:外貌、服装、标志物"
- [(ngModel)]="visualBible.subject" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <input class="dh-input" type="text" placeholder="产品:外观、卖点、品牌感(无产品可空)"
- [(ngModel)]="visualBible.product" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <input class="dh-input" type="text" placeholder="场景:地点、空间布局、时代"
- [(ngModel)]="visualBible.location" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <input class="dh-input" type="text" placeholder="色调:warm amber / cool teal 等"
- [(ngModel)]="visualBible.palette" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <input class="dh-input" type="text" placeholder="光线:golden hour / soft studio light 等"
- [(ngModel)]="visualBible.lighting" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- <input class="dh-input" type="text" placeholder="镜头基调:handheld / dolly / close-up 等"
- [(ngModel)]="visualBible.camera" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()">
- </div>
- <textarea class="dh-textarea t2v-bible-notes" rows="2" maxlength="240" *ngIf="continuityEnabled"
- placeholder="固定风格与禁止变化项:例如不要换衣服、不要改变产品颜色、不要切换地点"
- [(ngModel)]="visualBible.constraints" [disabled]="isRunning || hasInflightScene" (ngModelChange)="syncDraft()"></textarea>
- <!-- 主体 / 产品 / 场景 参考图:可选上传,传给即梦做生成参考 -->
- <div class="t2v-bible-refs" *ngIf="continuityEnabled">
- <div class="t2v-bible-refs__head">
- <span class="t2v-bible-refs__title">参考图(可选)</span>
- <span class="t2v-tip">上传后会作为画面参考;可在分镜中单独调整是否使用</span>
- </div>
- <div class="t2v-bible-refs__grid">
- <ng-container *ngFor="let f of bibleRefSlots">
- <div class="t2v-bible-ref">
- <div class="t2v-bible-ref__label">{{ f.label }}</div>
- <div class="t2v-bible-ref__box"
- [class.is-empty]="!bibleImageUrlFor(f.field)"
- [class.is-busy]="bibleImageUploading[f.field]">
- <ng-container *ngIf="bibleImageUrlFor(f.field); else emptySlot">
- <a [href]="bibleImageUrlFor(f.field)" target="_blank" rel="noopener" title="点击查看大图">
- <img [src]="bibleImageUrlFor(f.field)" [alt]="f.label" loading="lazy">
- </a>
- <button type="button" class="t2v-bible-ref__clear"
- [disabled]="isRunning || hasInflightScene || bibleImageUploading[f.field]"
- (click)="clearBibleImage(f.field)" title="移除参考图">×</button>
- </ng-container>
- <ng-template #emptySlot>
- <label class="t2v-bible-ref__add">
- <input type="file" accept="image/*" hidden
- [disabled]="isRunning || hasInflightScene || bibleImageUploading[f.field]"
- (change)="onBibleImagePick(f.field, $event)">
- <span *ngIf="!bibleImageUploading[f.field]">+ 上传</span>
- <span *ngIf="bibleImageUploading[f.field]">上传中…</span>
- </label>
- </ng-template>
- </div>
- </div>
- </ng-container>
- </div>
- <div class="t2v-bible-refs__error" *ngIf="bibleImageError">{{ bibleImageError }}</div>
- </div>
- </div>
- <!-- 画面类型:仅快速模式 -->
- <div class="dh-substep" *ngIf="quickMode">
- <div class="dh-substep__head">
- <h4>画面类型</h4>
- <span class="t2v-tip" *ngIf="mediaMode === 'video'">每段画面会生成动态视频</span>
- </div>
- <div class="dh-segment">
- <button class="dh-segment__item"
- [class.is-active]="mediaMode === 'image'"
- [disabled]="isRunning" (click)="mediaMode = 'image'">静态图(快)</button>
- <button class="dh-segment__item"
- [class.is-active]="mediaMode === 'video'"
- [disabled]="isRunning" (click)="mediaMode = 'video'">动态视频(慢)</button>
- </div>
- <ng-container *ngIf="mediaMode === 'video'">
- <span class="t2v-sublabel">单段时长</span>
- <div class="dh-segment">
- <button class="dh-segment__item"
- [class.is-active]="clipFrames === 121"
- [disabled]="isRunning" (click)="clipFrames = 121">5 秒</button>
- <button class="dh-segment__item"
- [class.is-active]="clipFrames === 241"
- [disabled]="isRunning" (click)="clipFrames = 241">10 秒</button>
- </div>
- <span class="t2v-sublabel">视频质量</span>
- <div class="dh-segment">
- <button class="dh-segment__item"
- [class.is-active]="clipQuality === '720p'"
- [disabled]="isRunning" (click)="clipQuality = '720p'">720p</button>
- <button class="dh-segment__item"
- [class.is-active]="clipQuality === '1080p'"
- [disabled]="isRunning" (click)="clipQuality = '1080p'">1080p</button>
- </div>
- </ng-container>
- </div>
- <!-- 手动模式专属:图生视频默认设置(应用到每张卡的「生成视频」按钮) -->
- <div class="dh-substep" *ngIf="!quickMode">
- <div class="dh-substep__head">
- <h4>图生视频默认设置</h4>
- <span class="t2v-tip">手动模式下每段「生成视频」时使用</span>
- </div>
- <span class="t2v-sublabel">单段时长</span>
- <div class="dh-segment">
- <button class="dh-segment__item"
- [class.is-active]="clipFrames === 121"
- [disabled]="isRunning || hasInflightScene" (click)="clipFrames = 121">5 秒</button>
- <button class="dh-segment__item"
- [class.is-active]="clipFrames === 241"
- [disabled]="isRunning || hasInflightScene" (click)="clipFrames = 241">10 秒</button>
- </div>
- <span class="t2v-sublabel">视频质量</span>
- <div class="dh-segment">
- <button class="dh-segment__item"
- [class.is-active]="clipQuality === '720p'"
- [disabled]="isRunning || hasInflightScene" (click)="clipQuality = '720p'">720p</button>
- <button class="dh-segment__item"
- [class.is-active]="clipQuality === '1080p'"
- [disabled]="isRunning || hasInflightScene" (click)="clipQuality = '1080p'">1080p</button>
- </div>
- </div>
- <!-- 手动模式:可选标题,移到底部 -->
- <div class="dh-substep" *ngIf="!quickMode">
- <div class="dh-substep__head">
- <h4>视频标题(可选)</h4>
- <span class="t2v-tip">仅作保存命名,不影响生成</span>
- </div>
- <input class="dh-input" type="text" maxlength="60"
- placeholder="例如:周末一日 vlog"
- [(ngModel)]="topic" [disabled]="isRunning">
- </div>
- </div>
- <!-- ========================= 中:分镜 ========================= -->
- <div class="dh-step-col">
- <div class="dh-step-col__head">
- <span class="dh-step-col__step">Step 2</span>
- <span class="dh-step-col__title">分镜与素材</span>
- </div>
- <!-- ============ 快速模式:只读卡片 ============ -->
- <ng-container *ngIf="quickMode; else manualBlock">
- <div *ngIf="!scenes.length && stage === 'idle'" class="t2v-empty">
- <p>填写主题并点击右侧 <strong>开始生成</strong>,AI 将自动产出分镜画面。</p>
- </div>
- <div *ngIf="!scenes.length && stage === 'scripting'" class="t2v-empty">
- <p>🤖 AI 正在拆分分镜...</p>
- </div>
- <ul class="t2v-scene-list" *ngIf="scenes.length">
- <li *ngFor="let s of scenes; let i = index" class="t2v-scene-item">
- <div class="t2v-scene-item__head">
- <span class="t2v-scene-item__seq">#{{ i + 1 }}</span>
- <span class="t2v-scene-item__state"
- [class.is-pending]="['image-pending','tail-pending','video-pending'].includes(s.state)"
- [class.is-done]="['image-done','tail-done','video-done'].includes(s.state)"
- [class.is-failed]="['image-failed','tail-failed','video-failed'].includes(s.state)">
- <ng-container [ngSwitch]="s.state">
- <span *ngSwitchCase="'idle'">等待</span>
- <span *ngSwitchCase="'image-pending'">🖼️ 生成画面中</span>
- <span *ngSwitchCase="'image-done'">画面 ✓</span>
- <span *ngSwitchCase="'image-failed'">画面 ✗</span>
- <span *ngSwitchCase="'video-pending'">🎬 图生视频中</span>
- <span *ngSwitchCase="'video-done'">视频 ✓</span>
- <span *ngSwitchCase="'video-failed'">视频 ✗ (降静图)</span>
- </ng-container>
- </span>
- <span class="t2v-scene-item__media" *ngIf="s.videoUrl || s.imageUrl">
- <ng-container *ngIf="s.videoUrl">🎬</ng-container>
- <ng-container *ngIf="!s.videoUrl && s.imageUrl">🖼️</ng-container>
- </span>
- </div>
- <div class="t2v-scene-item__body">
- <video *ngIf="s.videoUrl" [src]="s.videoUrl" class="t2v-scene-item__img" muted playsinline
- onmouseenter="this.play()" onmouseleave="this.pause()"></video>
- <img *ngIf="!s.videoUrl && s.imageUrl" [src]="s.imageUrl" alt="" class="t2v-scene-item__img">
- <div *ngIf="!s.imageUrl && !s.videoUrl" class="t2v-scene-item__img t2v-scene-item__img--ph"></div>
- <div class="t2v-scene-item__text">
- <p *ngIf="s.narration" class="t2v-scene-item__narration">{{ s.narration }}</p>
- <p class="t2v-scene-item__prompt" [title]="s.scenePrompt">{{ s.scenePrompt }}</p>
- <p *ngIf="s.error" class="t2v-scene-item__error">{{ s.error }}</p>
- </div>
- </div>
- </li>
- </ul>
- </ng-container>
- <!-- ============ 手动模式:可编辑卡片(首/尾帧 + 上传) ============ -->
- <ng-template #manualBlock>
- <div *ngIf="!scenes.length" class="t2v-empty">
- <p>点击下方 <strong>➕ 添加分镜</strong> 添加卡片。每卡可独立选「仅首帧」或「首尾帧」i2v 模式,每个帧位都支持 AI 生成或本地上传。</p>
- </div>
- <ul class="t2v-scene-list" *ngIf="scenes.length">
- <li *ngFor="let s of scenes; let i = index" class="t2v-scene-item t2v-scene-item--manual">
- <!-- 头部:序号 + 状态 + 删除 -->
- <div class="t2v-scene-item__head">
- <span class="t2v-scene-item__seq">#{{ i + 1 }}</span>
- <span class="t2v-scene-item__state"
- [class.is-pending]="['image-pending','tail-pending','video-pending'].includes(s.state)"
- [class.is-done]="['image-done','tail-done','video-done'].includes(s.state)"
- [class.is-failed]="['image-failed','tail-failed','video-failed'].includes(s.state)">
- <ng-container [ngSwitch]="s.state">
- <span *ngSwitchCase="'idle'">未开始</span>
- <span *ngSwitchCase="'image-pending'">⏳ 处理首帧...</span>
- <span *ngSwitchCase="'image-done'">首帧 ✓</span>
- <span *ngSwitchCase="'image-failed'">首帧 ✗</span>
- <span *ngSwitchCase="'tail-pending'">⏳ 处理尾帧...</span>
- <span *ngSwitchCase="'tail-done'">尾帧 ✓</span>
- <span *ngSwitchCase="'tail-failed'">尾帧 ✗</span>
- <span *ngSwitchCase="'video-pending'">🎬 生成视频...</span>
- <span *ngSwitchCase="'video-done'">视频 ✓</span>
- <span *ngSwitchCase="'video-failed'">视频 ✗</span>
- </ng-container>
- </span>
- <button class="t2v-scene-item__del" type="button"
- [disabled]="hasInflightScene"
- title="删除该分镜" (click)="removeScene(i)">✖</button>
- </div>
- <!-- 1. 画面描述(始终可编辑) -->
- <label class="t2v-card-label">画面描述</label>
- <textarea class="dh-textarea t2v-scene-item__edit" rows="2" maxlength="300"
- placeholder="中英文均可,推荐英文。例:a woman walks in dawn fog, golden hour, wide shot, slow zoom in"
- [(ngModel)]="s.scenePrompt"
- (ngModelChange)="syncDraft()"
- [disabled]="['image-pending','tail-pending','video-pending'].includes(s.state)"></textarea>
- <!-- 2. 视频模式选择 -->
- <div class="t2v-shot-controls" *ngIf="continuityEnabled">
- <label class="t2v-card-label">镜头类型</label>
- <div class="dh-segment dh-segment--wrap">
- <button class="dh-segment__item" type="button"
- *ngFor="let t of shotTypePresets"
- [class.is-active]="(s.shotType || 'action') === t.value"
- [disabled]="['image-pending','tail-pending','video-pending'].includes(s.state)"
- (click)="setSceneShotType(s, t.value)">
- {{ t.label }}
- </button>
- </div>
- <label class="t2v-card-label">继承内容</label>
- <div class="t2v-inherit-row">
- <label><input type="checkbox" [(ngModel)]="s.inheritSubject" (ngModelChange)="syncDraft()"> 主体</label>
- <label><input type="checkbox" [(ngModel)]="s.inheritLocation" (ngModelChange)="syncDraft()"> 场景</label>
- <label><input type="checkbox" [(ngModel)]="s.inheritStyle" (ngModelChange)="syncDraft()"> 风格</label>
- </div>
- <label class="t2v-card-label">参考图</label>
- <div class="dh-segment dh-segment--wrap">
- <button class="dh-segment__item" type="button"
- *ngFor="let r of referenceModePresets"
- [class.is-active]="(s.referenceMode || 'auto') === r.value"
- [disabled]="['image-pending','tail-pending','video-pending'].includes(s.state)"
- (click)="s.referenceMode = r.value; syncDraft()">
- {{ r.label }}
- </button>
- </div>
- </div>
- <div class="t2v-continuity-fields" *ngIf="continuityEnabled">
- <input class="dh-input" type="text" maxlength="180"
- placeholder="起始状态:承接上一镜,可留空"
- [(ngModel)]="s.startState" (ngModelChange)="syncDraft()"
- [disabled]="['image-pending','tail-pending','video-pending'].includes(s.state)">
- <input class="dh-input" type="text" maxlength="180"
- placeholder="结束状态:用于尾帧和下一镜,可留空"
- [(ngModel)]="s.endState" (ngModelChange)="syncDraft()"
- [disabled]="['image-pending','tail-pending','video-pending'].includes(s.state)">
- </div>
- <label class="t2v-card-label" style="margin-top:14px;">图生视频模式</label>
- <div class="dh-segment t2v-vmode-seg">
- <button class="dh-segment__item"
- [class.is-active]="(s.videoMode || 'first') === 'first'"
- [disabled]="hasInflightScene"
- (click)="setSceneVideoMode(i, 'first')">
- 仅首帧
- <span class="t2v-vmode-hint">1 张图 · 通用</span>
- </button>
- <button class="dh-segment__item"
- [class.is-active]="s.videoMode === 'first-last'"
- [disabled]="hasInflightScene"
- (click)="setSceneVideoMode(i, 'first-last')">
- 首尾帧
- <span class="t2v-vmode-hint">2 张图 · 更平滑</span>
- </button>
- </div>
- <!-- 3. 帧位编辑器:首帧 + (可选) 尾帧 -->
- <div class="t2v-frames" [class.t2v-frames--dual]="s.videoMode === 'first-last'">
- <!-- 首帧 -->
- <div class="t2v-frame-slot">
- <div class="t2v-frame-slot__head">
- <span class="t2v-frame-slot__title">首帧</span>
- <span class="t2v-frame-slot__source"
- *ngIf="s.imageUrl"
- [class.is-upload]="s.imageSource === 'upload'">
- {{ s.imageSource === 'upload' ? '上传' : 'AI' }}
- </span>
- </div>
- <!-- 缩略图 / 占位 / 加载 -->
- <div class="t2v-frame-slot__thumb">
- <div *ngIf="s.pendingTarget === 'head' && (s.state === 'image-pending')"
- class="t2v-frame-slot__loading">
- <span class="t2v-spin"></span>
- <span>处理中...</span>
- </div>
- <a *ngIf="s.imageUrl && s.pendingTarget !== 'head'"
- [href]="s.imageUrl" target="_blank" rel="noopener"
- title="点击查看原图">
- <img [src]="s.imageUrl" alt="首帧">
- </a>
- <div *ngIf="!s.imageUrl && s.pendingTarget !== 'head'" class="t2v-frame-slot__ph">
- <span>无首帧</span>
- </div>
- </div>
- <!-- 操作 -->
- <div class="t2v-frame-slot__ops">
- <button class="dh-chip-btn"
- [class.is-primary]="!s.imageUrl"
- [disabled]="!s.scenePrompt.trim() || hasInflightScene"
- (click)="regenerateImage(i)">
- <span *ngIf="!s.imageUrl">▶ AI 生成</span>
- <span *ngIf="s.imageUrl">🔄 AI 重生</span>
- </button>
- <input #headInput type="file" accept="image/*"
- hidden [disabled]="hasInflightScene"
- (change)="onSceneFileChange(i, 'head', $event)">
- <button class="dh-chip-btn"
- [disabled]="hasInflightScene"
- (click)="headInput.click()">📤 上传</button>
- <button class="dh-chip-btn"
- *ngIf="s.imageUrl"
- [disabled]="hasInflightScene"
- (click)="clearSceneImage(i, 'head')"
- title="清空首帧">×</button>
- </div>
- </div>
- <!-- 尾帧(仅首尾帧模式) -->
- <div class="t2v-frame-slot" *ngIf="s.videoMode === 'first-last'">
- <div class="t2v-frame-slot__head">
- <span class="t2v-frame-slot__title">尾帧</span>
- <span class="t2v-frame-slot__source"
- *ngIf="s.tailImageUrl"
- [class.is-upload]="s.tailImageSource === 'upload'">
- {{ s.tailImageSource === 'upload' ? '上传' : 'AI' }}
- </span>
- </div>
- <div class="t2v-frame-slot__thumb">
- <div *ngIf="s.pendingTarget === 'tail'"
- class="t2v-frame-slot__loading">
- <span class="t2v-spin"></span>
- <span>处理中...</span>
- </div>
- <a *ngIf="s.tailImageUrl && s.pendingTarget !== 'tail'"
- [href]="s.tailImageUrl" target="_blank" rel="noopener"
- title="点击查看原图">
- <img [src]="s.tailImageUrl" alt="尾帧">
- </a>
- <div *ngIf="!s.tailImageUrl && s.pendingTarget !== 'tail'" class="t2v-frame-slot__ph">
- <span>无尾帧</span>
- </div>
- </div>
- <div class="t2v-frame-slot__ops">
- <button class="dh-chip-btn"
- [class.is-primary]="!s.tailImageUrl"
- [disabled]="!s.scenePrompt.trim() || hasInflightScene"
- (click)="regenerateTailImage(i)">
- <span *ngIf="!s.tailImageUrl">▶ AI 生成</span>
- <span *ngIf="s.tailImageUrl">🔄 AI 重生</span>
- </button>
- <input #tailInput type="file" accept="image/*"
- hidden [disabled]="hasInflightScene"
- (change)="onSceneFileChange(i, 'tail', $event)">
- <button class="dh-chip-btn"
- [disabled]="hasInflightScene"
- (click)="tailInput.click()">📤 上传</button>
- <button class="dh-chip-btn"
- *ngIf="s.tailImageUrl"
- [disabled]="hasInflightScene"
- (click)="clearSceneImage(i, 'tail')"
- title="清空尾帧">×</button>
- </div>
- </div>
- </div>
- <!-- 4. 视频预览(已生成时) -->
- <div *ngIf="s.videoUrl" class="t2v-video-preview">
- <video [src]="s.videoUrl" controls muted playsinline></video>
- </div>
- <!-- 5. 错误 -->
- <p *ngIf="s.error" class="t2v-scene-item__error">{{ s.error }}</p>
- <!-- 6. 视频操作 -->
- <div class="t2v-scene-item__actions">
- <ng-container *ngIf="s.imageUrl && (s.videoMode !== 'first-last' || s.tailImageUrl)">
- <button class="dh-chip-btn is-primary"
- [disabled]="hasInflightScene"
- (click)="regenerateVideo(i)">
- <span *ngIf="!s.videoUrl">🎬 生成视频</span>
- <span *ngIf="s.videoUrl">🔄 重生视频</span>
- </button>
- </ng-container>
- <span *ngIf="!s.imageUrl" class="t2v-tip">先生成或上传首帧</span>
- <span *ngIf="s.imageUrl && s.videoMode === 'first-last' && !s.tailImageUrl" class="t2v-tip">首尾帧模式需要尾帧</span>
- </div>
- </li>
- </ul>
- <button class="t2v-add-scene" type="button"
- [disabled]="hasInflightScene"
- (click)="addManualScene()">➕ 添加分镜</button>
- </ng-template>
- </div>
- <!-- ========================= 右:操作与结果 ========================= -->
- <div class="dh-step-col">
- <div class="dh-step-col__head">
- <span class="dh-step-col__step">Step 3</span>
- <span class="dh-step-col__title">生成与预览</span>
- </div>
- <div class="dh-substep pl-action-area">
- <button class="pl-cta-primary"
- type="button"
- [disabled]="!canStart"
- [attr.data-loading]="isRunning ? 'true' : null"
- (click)="start()">
- <span *ngIf="!isRunning && !resultVideoUrl">
- {{ quickMode ? '▶ 开始生成视频' : '▶ 合成最终视频' }}
- </span>
- <span *ngIf="!isRunning && resultVideoUrl">🔄 重新合成</span>
- <span *ngIf="isRunning">{{ totalProgress }}% {{ quickMode ? '生成中' : '合成中' }}</span>
- </button>
- <button *ngIf="isRunning" class="dh-chip-btn" (click)="cancel()">取消</button>
- </div>
- <!-- 手动模式:当前有几段已就绪 -->
- <div *ngIf="!quickMode && scenes.length > 0" class="dh-substep">
- <div class="dh-substep__head"><h4>分镜状态</h4></div>
- <div class="t2v-scene-summary">
- <span class="t2v-scene-summary__chip">
- 共 <strong>{{ scenes.length }}</strong> 段
- </span>
- <span class="t2v-scene-summary__chip is-ok">
- ✓ 已就绪 <strong>{{ readyCount }}</strong>
- </span>
- <span class="t2v-scene-summary__chip" *ngIf="videoReadyCount > 0">
- 🎬 含视频 <strong>{{ videoReadyCount }}</strong>
- </span>
- </div>
- </div>
- <div *ngIf="stage !== 'idle'" class="dh-substep">
- <div class="dh-substep__head"><h4>总体进度</h4></div>
- <div class="pl-progress">
- <div class="pl-progress__bar"><div class="pl-progress__fill" [style.width.%]="totalProgress"></div></div>
- <div class="pl-progress__text">
- <span>{{ stageStatus || '准备中...' }}</span>
- <span class="pl-progress__pct">{{ totalProgress }}%</span>
- </div>
- </div>
- <ul class="t2v-stage-list">
- <li *ngIf="quickMode"
- [class.is-active]="stage === 'scripting'"
- [class.is-done]="['imaging','videoing','composing','done'].includes(stage)">
- <span class="t2v-stage-list__dot"></span>
- <span>📝 AI 分镜拆分</span>
- </li>
- <li *ngIf="quickMode"
- [class.is-active]="stage === 'imaging'"
- [class.is-done]="['videoing','composing','done'].includes(stage)">
- <span class="t2v-stage-list__dot"></span>
- <span>🖼️ 生成画面({{ imageProgress }}%)</span>
- </li>
- <li *ngIf="quickMode && mediaMode === 'video'"
- [class.is-active]="stage === 'videoing'"
- [class.is-done]="['composing','done'].includes(stage)">
- <span class="t2v-stage-list__dot"></span>
- <span>🎞️ 生成动态片段({{ videoProgress }}%)</span>
- </li>
- <li [class.is-active]="stage === 'composing'" [class.is-done]="stage === 'done'">
- <span class="t2v-stage-list__dot"></span>
- <span>🎬 合成视频({{ composeProgress }}%)</span>
- </li>
- </ul>
- </div>
- <div *ngIf="errorMsg" class="pl-error-card">
- <strong>失败:</strong>{{ errorMsg }}
- </div>
- <div *ngIf="resultVideoUrl" class="dh-substep">
- <div class="dh-substep__head"><h4>生成结果</h4></div>
- <video class="pl-result-video" [src]="resultVideoUrl" controls playsinline></video>
- <div class="t2v-case-summary" *ngIf="caseSummary.total > 0">
- <span class="t2v-case-summary__label">合成明细:</span>
- <span class="t2v-case-chip" *ngIf="caseSummary.C" title="动态视频段">动态视频 × {{ caseSummary.C }}</span>
- <span class="t2v-case-chip" *ngIf="caseSummary.D" title="静态画面段">静态画面 × {{ caseSummary.D }}</span>
- </div>
- <div class="pl-result-actions">
- <a class="dh-chip-btn" [href]="resultVideoUrl" target="_blank" rel="noopener">在新窗口打开</a>
- <a class="dh-chip-btn" [href]="resultVideoUrl" [download]="(topic || 'topic-to-video') + '.mp4'">下载视频</a>
- </div>
- </div>
- </div>
- </div>
- </section>
|