image-to-video.component.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <section class="dh-page pl-page i2v-page">
  2. <app-session-bar
  3. pipelineId="image-to-video"
  4. pipelineLabel="图生视频"
  5. (newSession)="onNewSession()"
  6. (openSession)="onOpenSession($event)">
  7. </app-session-bar>
  8. <header class="dh-page-header">
  9. <div class="dh-header-icon">
  10. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  11. <rect x="3" y="3" width="18" height="14" rx="2"></rect>
  12. <circle cx="9" cy="9" r="1.5"></circle>
  13. <polyline points="21 15 16 10 5 21"></polyline>
  14. </svg>
  15. </div>
  16. <div class="dh-header-text">
  17. <h2>图生视频</h2>
  18. <p>上传一张(首帧)或两张(首尾帧)图片,搭配提示词生成动态视频。</p>
  19. </div>
  20. </header>
  21. <div class="pl-grid">
  22. <!-- 左列:素材上传 + 模式切换 -->
  23. <div class="dh-step-col">
  24. <div class="dh-step-col__head">
  25. <span class="dh-step-col__step">Step 1</span>
  26. <span class="dh-step-col__title">上传参考图片</span>
  27. </div>
  28. <div class="dh-substep">
  29. <div class="dh-substep__head"><h4>生成模式</h4></div>
  30. <div class="dh-segment">
  31. <button class="dh-segment__item" [class.is-active]="mode === '2'" (click)="setMode('2')">首帧</button>
  32. <button class="dh-segment__item" [class.is-active]="mode === '3'" (click)="setMode('3')">首尾帧</button>
  33. <button class="dh-segment__item" [class.is-active]="mode === '4'" (click)="setMode('4')">运镜</button>
  34. <button class="dh-segment__item" [class.is-active]="mode === 'reference'" (click)="setMode('reference')">参考图生成</button>
  35. </div>
  36. <p class="i2v-mode-hint" *ngIf="mode === '2'">单张图 + 提示词 → 以该图为首帧生成动态视频</p>
  37. <p class="i2v-mode-hint" *ngIf="mode === '3'">两张图 → 以首帧/尾帧为边界,自动补全中间过渡 <strong>(仅 720P / 1080P)</strong></p>
  38. <p class="i2v-mode-hint" *ngIf="mode === '4'">一张图 + 运镜模板 → 预设镜头运动生成视频 <strong>(仅 720P)</strong></p>
  39. <p class="i2v-mode-hint" *ngIf="mode === 'reference'">上传参考图片并描述创意方向,系统会延续主体与风格生成视频。</p>
  40. </div>
  41. <!-- 首帧 -->
  42. <div class="dh-substep">
  43. <div class="dh-substep__head"><h4>{{ mode === 'reference' ? '参考图片' : '首帧图片' }} <em>*</em></h4></div>
  44. <input type="file" accept="image/*" hidden #firstInput
  45. (change)="onFilePicked($event, 'first')">
  46. <div *ngIf="!firstFrame" class="dh-image-card" (click)="triggerFilePick('first', firstInput)">
  47. <div class="dh-image-card__icon">
  48. <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  49. <rect x="3" y="3" width="18" height="14" rx="2"></rect>
  50. <circle cx="9" cy="9" r="1.5"></circle>
  51. <polyline points="21 15 16 10 5 21"></polyline>
  52. </svg>
  53. </div>
  54. <div class="dh-image-card__title">
  55. {{ uploadingSlot === 'first' ? '上传中…' : (mode === 'reference' ? '点击上传参考图片' : '点击上传首帧图片') }}
  56. </div>
  57. <div class="dh-image-card__hints">支持 JPG / PNG / WebP,≤10MB</div>
  58. </div>
  59. <div *ngIf="firstFrame" class="pl-preview-card">
  60. <img [src]="firstFrame.preview" alt="first frame">
  61. <div class="pl-preview-meta">
  62. <div class="dh-image-card__name">{{ firstFrame.name }}</div>
  63. <div class="dh-image-card__size">{{ formatSize(firstFrame.size) }}</div>
  64. </div>
  65. <div class="pl-preview-actions">
  66. <button class="dh-chip-btn" (click)="triggerFilePick('first', firstInput)">更换</button>
  67. <button class="dh-chip-btn" (click)="removeFrame('first')">移除</button>
  68. </div>
  69. </div>
  70. </div>
  71. <!-- 尾帧(仅 mode=3) -->
  72. <div class="dh-substep" *ngIf="mode === '3'">
  73. <div class="dh-substep__head"><h4>尾帧图片 <em>*</em></h4></div>
  74. <input type="file" accept="image/*" hidden #lastInput
  75. (change)="onFilePicked($event, 'last')">
  76. <div *ngIf="!lastFrame" class="dh-image-card" (click)="triggerFilePick('last', lastInput)">
  77. <div class="dh-image-card__icon">
  78. <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  79. <rect x="3" y="3" width="18" height="14" rx="2"></rect>
  80. <circle cx="9" cy="9" r="1.5"></circle>
  81. <polyline points="21 15 16 10 5 21"></polyline>
  82. </svg>
  83. </div>
  84. <div class="dh-image-card__title">
  85. {{ uploadingSlot === 'last' ? '上传中…' : '点击上传尾帧图片' }}
  86. </div>
  87. </div>
  88. <div *ngIf="lastFrame" class="pl-preview-card">
  89. <img [src]="lastFrame.preview" alt="last frame">
  90. <div class="pl-preview-meta">
  91. <div class="dh-image-card__name">{{ lastFrame.name }}</div>
  92. <div class="dh-image-card__size">{{ formatSize(lastFrame.size) }}</div>
  93. </div>
  94. <div class="pl-preview-actions">
  95. <button class="dh-chip-btn" (click)="triggerFilePick('last', lastInput)">更换</button>
  96. <button class="dh-chip-btn" (click)="removeFrame('last')">移除</button>
  97. </div>
  98. </div>
  99. </div>
  100. <div *ngIf="uploadError" class="pl-error-card">{{ uploadError }}</div>
  101. </div>
  102. <!-- 中列:提示词 + 参数 -->
  103. <div class="dh-step-col">
  104. <div class="dh-step-col__head">
  105. <span class="dh-step-col__step">Step 2</span>
  106. <span class="dh-step-col__title">提示词与参数</span>
  107. </div>
  108. <div class="dh-substep">
  109. <div class="dh-substep__head">
  110. <h4>提示词 <em *ngIf="mode !== '4'">*</em><span *ngIf="mode === '4'" class="pl-optional-flag">(运镜模式可选)</span></h4>
  111. </div>
  112. <div class="dh-field">
  113. <textarea class="dh-textarea" rows="6"
  114. placeholder="描述你希望视频如何呈现,例如:人物缓慢转头微笑,背景樱花飘落..."
  115. [(ngModel)]="prompt" (ngModelChange)="syncDraft()"
  116. [disabled]="generating"></textarea>
  117. <div class="dh-field__counter">{{ prompt.length }} 字</div>
  118. </div>
  119. <div class="dh-prompt-presets">
  120. <span class="dh-prompt-presets__label">快捷追加:</span>
  121. <button class="dh-chip-btn" *ngFor="let p of promptPresets"
  122. type="button"
  123. [disabled]="generating"
  124. (click)="applyPreset(p.value)">
  125. {{ p.label }}
  126. </button>
  127. </div>
  128. </div>
  129. <div class="dh-substep">
  130. <div class="dh-substep__head"><h4>视频时长</h4></div>
  131. <div class="dh-segment">
  132. <button class="dh-segment__item" *ngFor="let sec of durationOptions"
  133. [class.is-active]="durationSeconds === sec"
  134. [disabled]="generating" (click)="setDurationSeconds(sec)">{{sec}} 秒</button>
  135. </div>
  136. <input class="dh-input" type="number" min="3" max="15" step="1"
  137. [ngModel]="durationSeconds"
  138. [disabled]="generating"
  139. (ngModelChange)="setDurationSeconds($event)"
  140. placeholder="自定义秒数">
  141. </div>
  142. <div class="dh-substep">
  143. <div class="dh-substep__head"><h4>分辨率</h4></div>
  144. <div class="dh-segment">
  145. <button class="dh-segment__item" [class.is-active]="quality === '720p'"
  146. [disabled]="generating" (click)="setQuality('720p')">720P</button>
  147. <button class="dh-segment__item" [class.is-active]="quality === '1080p'"
  148. [disabled]="generating || mode === '4'" (click)="setQuality('1080p')"
  149. [title]="mode === '4' ? '运镜模式仅 720P 可用' : ''">1080P</button>
  150. <button class="dh-segment__item" [class.is-active]="quality === 'pro'"
  151. [disabled]="generating || mode === '3' || mode === '4'" (click)="setQuality('pro')"
  152. [title]="mode === '3' || mode === '4' ? 'Pro 仅支持首帧和参考图生成模式' : ''">Pro</button>
  153. </div>
  154. </div>
  155. <!-- 运镜模式专用参数 -->
  156. <div class="dh-substep" *ngIf="mode === '4'">
  157. <div class="dh-substep__head"><h4>运镜模板 <em>*</em></h4></div>
  158. <div class="i2v-template-grid">
  159. <button *ngFor="let t of cameraTemplates"
  160. type="button"
  161. class="i2v-template-card"
  162. [class.is-active]="cameraTemplate === t.id"
  163. [disabled]="generating"
  164. [title]="t.hint"
  165. (click)="cameraTemplate = t.id">
  166. <span class="i2v-template-card__label">{{ t.label }}</span>
  167. <span class="i2v-template-card__hint">{{ t.hint }}</span>
  168. </button>
  169. </div>
  170. </div>
  171. <div class="dh-substep" *ngIf="mode === '4'">
  172. <div class="dh-substep__head"><h4>运镜强度</h4></div>
  173. <div class="dh-segment">
  174. <button class="dh-segment__item" [class.is-active]="cameraStrength === 'weak'"
  175. [disabled]="generating" (click)="cameraStrength = 'weak'">弱</button>
  176. <button class="dh-segment__item" [class.is-active]="cameraStrength === 'medium'"
  177. [disabled]="generating" (click)="cameraStrength = 'medium'">中</button>
  178. <button class="dh-segment__item" [class.is-active]="cameraStrength === 'strong'"
  179. [disabled]="generating" (click)="cameraStrength = 'strong'">强</button>
  180. </div>
  181. </div>
  182. <!-- 画面比例:Pro 首帧模式用于视频比例;参考图生成模式用于创意画面比例 -->
  183. <div class="dh-substep" *ngIf="(mode === '2' && quality === 'pro') || mode === 'reference'">
  184. <div class="dh-substep__head"><h4>画面比例</h4></div>
  185. <div class="dh-segment dh-segment--wrap">
  186. <button class="dh-segment__item" [class.is-active]="aspect === '16:9'"
  187. [disabled]="generating" (click)="aspect = '16:9'">16:9</button>
  188. <button class="dh-segment__item" [class.is-active]="aspect === '9:16'"
  189. [disabled]="generating" (click)="aspect = '9:16'">9:16</button>
  190. <button class="dh-segment__item" [class.is-active]="aspect === '1:1'"
  191. [disabled]="generating" (click)="aspect = '1:1'">1:1</button>
  192. <button class="dh-segment__item" [class.is-active]="aspect === '4:3'"
  193. [disabled]="generating" (click)="aspect = '4:3'">4:3</button>
  194. <button class="dh-segment__item" [class.is-active]="aspect === '3:4'"
  195. [disabled]="generating" (click)="aspect = '3:4'">3:4</button>
  196. <button class="dh-segment__item" [class.is-active]="aspect === '21:9'"
  197. [disabled]="generating" (click)="aspect = '21:9'">21:9</button>
  198. </div>
  199. </div>
  200. </div>
  201. <!-- 右列:生成 + 预览 -->
  202. <div class="dh-step-col">
  203. <div class="dh-step-col__head">
  204. <span class="dh-step-col__step">Step 3</span>
  205. <span class="dh-step-col__title">生成与预览</span>
  206. </div>
  207. <div class="dh-substep pl-action-area">
  208. <button class="pl-cta-primary"
  209. type="button"
  210. [disabled]="!canGenerate"
  211. [attr.data-loading]="generating ? 'true' : null"
  212. (click)="generate()">
  213. <span *ngIf="!generating">▶ 开始生成</span>
  214. <span *ngIf="generating">生成中… {{ progress }}%</span>
  215. </button>
  216. <button *ngIf="generating" class="dh-chip-btn" (click)="cancel()">取消</button>
  217. <button *ngIf="resultVideoUrl && !generating" class="dh-chip-btn" (click)="reset()">重新生成</button>
  218. </div>
  219. <div *ngIf="generating || statusText" class="dh-substep">
  220. <div class="dh-substep__head"><h4>生成进度</h4></div>
  221. <div class="pl-progress">
  222. <div class="pl-progress__bar">
  223. <div class="pl-progress__fill" [style.width.%]="progress"></div>
  224. </div>
  225. <div class="pl-progress__text">
  226. <span>{{ statusText || '等待中...' }}</span>
  227. <span class="pl-progress__pct">{{ progress }}%</span>
  228. </div>
  229. </div>
  230. </div>
  231. <div *ngIf="referenceGeneratedFrameUrl" class="dh-substep i2v-reference-plan">
  232. <div class="dh-substep__head"><h4>创意画面预览</h4></div>
  233. <img *ngIf="referenceGeneratedFrameUrl" class="i2v-reference-frame" [src]="referenceGeneratedFrameUrl" alt="generated first frame">
  234. </div>
  235. <div *ngIf="errorMsg" class="pl-error-card">
  236. <strong>生成失败:</strong>{{ errorMsg }}
  237. </div>
  238. <div *ngIf="resultVideoUrl" class="dh-substep">
  239. <div class="dh-substep__head"><h4>生成结果</h4></div>
  240. <video class="pl-result-video" [src]="resultVideoUrl" controls playsinline></video>
  241. <div class="pl-result-actions">
  242. <a class="dh-chip-btn" [href]="resultVideoUrl" target="_blank" rel="noopener">在新窗口打开</a>
  243. <a class="dh-chip-btn" [href]="resultVideoUrl" [download]="'i2v-' + resultWorkId + '.mp4'">下载视频</a>
  244. </div>
  245. </div>
  246. </div>
  247. </div>
  248. </section>