| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- <section class="dh-page pl-page i2v-page">
- <app-session-bar
- pipelineId="image-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">
- <rect x="3" y="3" width="18" height="14" rx="2"></rect>
- <circle cx="9" cy="9" r="1.5"></circle>
- <polyline points="21 15 16 10 5 21"></polyline>
- </svg>
- </div>
- <div class="dh-header-text">
- <h2>图生视频</h2>
- <p>上传一张(首帧)或两张(首尾帧)图片,搭配提示词生成动态视频。</p>
- </div>
- </header>
- <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">上传参考图片</span>
- </div>
- <div class="dh-substep">
- <div class="dh-substep__head"><h4>生成模式</h4></div>
- <div class="dh-segment">
- <button class="dh-segment__item" [class.is-active]="mode === '2'" (click)="setMode('2')">首帧</button>
- <button class="dh-segment__item" [class.is-active]="mode === '3'" (click)="setMode('3')">首尾帧</button>
- <button class="dh-segment__item" [class.is-active]="mode === '4'" (click)="setMode('4')">运镜</button>
- <button class="dh-segment__item" [class.is-active]="mode === 'reference'" (click)="setMode('reference')">参考图生成</button>
- </div>
- <p class="i2v-mode-hint" *ngIf="mode === '2'">单张图 + 提示词 → 以该图为首帧生成动态视频</p>
- <p class="i2v-mode-hint" *ngIf="mode === '3'">两张图 → 以首帧/尾帧为边界,自动补全中间过渡 <strong>(仅 720P / 1080P)</strong></p>
- <p class="i2v-mode-hint" *ngIf="mode === '4'">一张图 + 运镜模板 → 预设镜头运动生成视频 <strong>(仅 720P)</strong></p>
- <p class="i2v-mode-hint" *ngIf="mode === 'reference'">上传参考图片并描述创意方向,系统会延续主体与风格生成视频。</p>
- </div>
- <!-- 首帧 -->
- <div class="dh-substep">
- <div class="dh-substep__head"><h4>{{ mode === 'reference' ? '参考图片' : '首帧图片' }} <em>*</em></h4></div>
- <input type="file" accept="image/*" hidden #firstInput
- (change)="onFilePicked($event, 'first')">
- <div *ngIf="!firstFrame" class="dh-image-card" (click)="triggerFilePick('first', firstInput)">
- <div class="dh-image-card__icon">
- <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <rect x="3" y="3" width="18" height="14" rx="2"></rect>
- <circle cx="9" cy="9" r="1.5"></circle>
- <polyline points="21 15 16 10 5 21"></polyline>
- </svg>
- </div>
- <div class="dh-image-card__title">
- {{ uploadingSlot === 'first' ? '上传中…' : (mode === 'reference' ? '点击上传参考图片' : '点击上传首帧图片') }}
- </div>
- <div class="dh-image-card__hints">支持 JPG / PNG / WebP,≤10MB</div>
- </div>
- <div *ngIf="firstFrame" class="pl-preview-card">
- <img [src]="firstFrame.preview" alt="first frame">
- <div class="pl-preview-meta">
- <div class="dh-image-card__name">{{ firstFrame.name }}</div>
- <div class="dh-image-card__size">{{ formatSize(firstFrame.size) }}</div>
- </div>
- <div class="pl-preview-actions">
- <button class="dh-chip-btn" (click)="triggerFilePick('first', firstInput)">更换</button>
- <button class="dh-chip-btn" (click)="removeFrame('first')">移除</button>
- </div>
- </div>
- </div>
- <!-- 尾帧(仅 mode=3) -->
- <div class="dh-substep" *ngIf="mode === '3'">
- <div class="dh-substep__head"><h4>尾帧图片 <em>*</em></h4></div>
- <input type="file" accept="image/*" hidden #lastInput
- (change)="onFilePicked($event, 'last')">
- <div *ngIf="!lastFrame" class="dh-image-card" (click)="triggerFilePick('last', lastInput)">
- <div class="dh-image-card__icon">
- <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <rect x="3" y="3" width="18" height="14" rx="2"></rect>
- <circle cx="9" cy="9" r="1.5"></circle>
- <polyline points="21 15 16 10 5 21"></polyline>
- </svg>
- </div>
- <div class="dh-image-card__title">
- {{ uploadingSlot === 'last' ? '上传中…' : '点击上传尾帧图片' }}
- </div>
- </div>
- <div *ngIf="lastFrame" class="pl-preview-card">
- <img [src]="lastFrame.preview" alt="last frame">
- <div class="pl-preview-meta">
- <div class="dh-image-card__name">{{ lastFrame.name }}</div>
- <div class="dh-image-card__size">{{ formatSize(lastFrame.size) }}</div>
- </div>
- <div class="pl-preview-actions">
- <button class="dh-chip-btn" (click)="triggerFilePick('last', lastInput)">更换</button>
- <button class="dh-chip-btn" (click)="removeFrame('last')">移除</button>
- </div>
- </div>
- </div>
- <div *ngIf="uploadError" class="pl-error-card">{{ uploadError }}</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>
- <div class="dh-substep">
- <div class="dh-substep__head">
- <h4>提示词 <em *ngIf="mode !== '4'">*</em><span *ngIf="mode === '4'" class="pl-optional-flag">(运镜模式可选)</span></h4>
- </div>
- <div class="dh-field">
- <textarea class="dh-textarea" rows="6"
- placeholder="描述你希望视频如何呈现,例如:人物缓慢转头微笑,背景樱花飘落..."
- [(ngModel)]="prompt" (ngModelChange)="syncDraft()"
- [disabled]="generating"></textarea>
- <div class="dh-field__counter">{{ prompt.length }} 字</div>
- </div>
- <div class="dh-prompt-presets">
- <span class="dh-prompt-presets__label">快捷追加:</span>
- <button class="dh-chip-btn" *ngFor="let p of promptPresets"
- type="button"
- [disabled]="generating"
- (click)="applyPreset(p.value)">
- {{ p.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 sec of durationOptions"
- [class.is-active]="durationSeconds === sec"
- [disabled]="generating" (click)="setDurationSeconds(sec)">{{sec}} 秒</button>
- </div>
- <input class="dh-input" type="number" min="3" max="15" step="1"
- [ngModel]="durationSeconds"
- [disabled]="generating"
- (ngModelChange)="setDurationSeconds($event)"
- placeholder="自定义秒数">
- </div>
- <div class="dh-substep">
- <div class="dh-substep__head"><h4>分辨率</h4></div>
- <div class="dh-segment">
- <button class="dh-segment__item" [class.is-active]="quality === '720p'"
- [disabled]="generating" (click)="setQuality('720p')">720P</button>
- <button class="dh-segment__item" [class.is-active]="quality === '1080p'"
- [disabled]="generating || mode === '4'" (click)="setQuality('1080p')"
- [title]="mode === '4' ? '运镜模式仅 720P 可用' : ''">1080P</button>
- <button class="dh-segment__item" [class.is-active]="quality === 'pro'"
- [disabled]="generating || mode === '3' || mode === '4'" (click)="setQuality('pro')"
- [title]="mode === '3' || mode === '4' ? 'Pro 仅支持首帧和参考图生成模式' : ''">Pro</button>
- </div>
- </div>
- <!-- 运镜模式专用参数 -->
- <div class="dh-substep" *ngIf="mode === '4'">
- <div class="dh-substep__head"><h4>运镜模板 <em>*</em></h4></div>
- <div class="i2v-template-grid">
- <button *ngFor="let t of cameraTemplates"
- type="button"
- class="i2v-template-card"
- [class.is-active]="cameraTemplate === t.id"
- [disabled]="generating"
- [title]="t.hint"
- (click)="cameraTemplate = t.id">
- <span class="i2v-template-card__label">{{ t.label }}</span>
- <span class="i2v-template-card__hint">{{ t.hint }}</span>
- </button>
- </div>
- </div>
- <div class="dh-substep" *ngIf="mode === '4'">
- <div class="dh-substep__head"><h4>运镜强度</h4></div>
- <div class="dh-segment">
- <button class="dh-segment__item" [class.is-active]="cameraStrength === 'weak'"
- [disabled]="generating" (click)="cameraStrength = 'weak'">弱</button>
- <button class="dh-segment__item" [class.is-active]="cameraStrength === 'medium'"
- [disabled]="generating" (click)="cameraStrength = 'medium'">中</button>
- <button class="dh-segment__item" [class.is-active]="cameraStrength === 'strong'"
- [disabled]="generating" (click)="cameraStrength = 'strong'">强</button>
- </div>
- </div>
- <!-- 画面比例:Pro 首帧模式用于视频比例;参考图生成模式用于创意画面比例 -->
- <div class="dh-substep" *ngIf="(mode === '2' && quality === 'pro') || mode === 'reference'">
- <div class="dh-substep__head"><h4>画面比例</h4></div>
- <div class="dh-segment dh-segment--wrap">
- <button class="dh-segment__item" [class.is-active]="aspect === '16:9'"
- [disabled]="generating" (click)="aspect = '16:9'">16:9</button>
- <button class="dh-segment__item" [class.is-active]="aspect === '9:16'"
- [disabled]="generating" (click)="aspect = '9:16'">9:16</button>
- <button class="dh-segment__item" [class.is-active]="aspect === '1:1'"
- [disabled]="generating" (click)="aspect = '1:1'">1:1</button>
- <button class="dh-segment__item" [class.is-active]="aspect === '4:3'"
- [disabled]="generating" (click)="aspect = '4:3'">4:3</button>
- <button class="dh-segment__item" [class.is-active]="aspect === '3:4'"
- [disabled]="generating" (click)="aspect = '3:4'">3:4</button>
- <button class="dh-segment__item" [class.is-active]="aspect === '21:9'"
- [disabled]="generating" (click)="aspect = '21:9'">21:9</button>
- </div>
- </div>
- </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]="!canGenerate"
- [attr.data-loading]="generating ? 'true' : null"
- (click)="generate()">
- <span *ngIf="!generating">▶ 开始生成</span>
- <span *ngIf="generating">生成中… {{ progress }}%</span>
- </button>
- <button *ngIf="generating" class="dh-chip-btn" (click)="cancel()">取消</button>
- <button *ngIf="resultVideoUrl && !generating" class="dh-chip-btn" (click)="reset()">重新生成</button>
- </div>
- <div *ngIf="generating || statusText" 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.%]="progress"></div>
- </div>
- <div class="pl-progress__text">
- <span>{{ statusText || '等待中...' }}</span>
- <span class="pl-progress__pct">{{ progress }}%</span>
- </div>
- </div>
- </div>
- <div *ngIf="referenceGeneratedFrameUrl" class="dh-substep i2v-reference-plan">
- <div class="dh-substep__head"><h4>创意画面预览</h4></div>
- <img *ngIf="referenceGeneratedFrameUrl" class="i2v-reference-frame" [src]="referenceGeneratedFrameUrl" alt="generated first frame">
- </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="pl-result-actions">
- <a class="dh-chip-btn" [href]="resultVideoUrl" target="_blank" rel="noopener">在新窗口打开</a>
- <a class="dh-chip-btn" [href]="resultVideoUrl" [download]="'i2v-' + resultWorkId + '.mp4'">下载视频</a>
- </div>
- </div>
- </div>
- </div>
- </section>
|