| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
- import { JimengService } from '../../services/jimeng.service';
- import { PortraitPipelineService, PortraitPipelineResult } from '../../services/portrait-pipeline.service';
- export type PortraitPrepareMode = 'raw' | 'benchmark';
- export interface PortraitPrepareOutput {
- /** 最终用于数字人生成的形象图 URL(raw 模式则为空,调用方自行用原 file 上传) */
- portraitUrl: string;
- /** 中间产物:规范化全身图 URL(benchmark 模式才有值) */
- fullBodyUrl: string;
- /** 对标提示词文本(benchmark 模式才有值) */
- benchmarkSummary: string;
- /** 当前模式 */
- mode: PortraitPrepareMode;
- }
- /**
- * 数字人形象图准备子组件(v2,已移除全身图生成步骤):
- * - raw:直接使用上传图(不调用 AI)
- * - benchmark:上传参考图 + Gemini 反推姿态/场景 + jimeng-img-v4 人物替换
- * (用上传形象图中的人物替换参考图中的人物,保留参考图姿态/场景)
- *
- * 仅在 selfieFile 存在时才显示模式切换 UI。
- */
- @Component({
- selector: 'app-portrait-prepare',
- standalone: true,
- imports: [CommonModule, FormsModule],
- template: `
- <div class="pp-container" *ngIf="selfieFile">
- <label class="pp-label">形象图准备方式</label>
- <div class="pp-mode-grid">
- <div class="pp-mode-card"
- [class.active]="mode === 'raw'"
- [class.disabled]="running"
- (click)="setMode('raw')">
- <div class="pp-mode-title">直接使用上传图</div>
- <div class="pp-mode-desc">沿用原始上传的人物形象图,不做 AI 处理。</div>
- </div>
- <div class="pp-mode-card"
- [class.active]="mode === 'benchmark'"
- [class.disabled]="running"
- (click)="setMode('benchmark')">
- <div class="pp-mode-title">参考图人物替换</div>
- <div class="pp-mode-desc">上传参考图,用您上传形象图中的人物替换参考图人物,保留参考图的姿态、动作与场景。</div>
- </div>
- </div>
- <!-- 参考图(提供姿态/场景)上传 -->
- <div *ngIf="mode === 'benchmark'" class="pp-benchmark">
- <label class="pp-label-sub">参考图(提供姿态、动作与场景)</label>
- <input type="file"
- #benchmarkInput
- accept="image/jpeg,image/png,image/jpg,image/gif,image/webp,.jpg,.jpeg,.png,.gif,.webp"
- (change)="onBenchmarkSelected($event)"
- style="display:none">
- <div class="pp-empty" *ngIf="!benchmarkPreviewUrl">
- <svg class="pp-svg pp-svg-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
- <rect x="3" y="3" width="18" height="18" rx="2"/>
- <circle cx="8.5" cy="10" r="1.5"/>
- <path d="M21 15l-5-5L5 21"/>
- </svg>
- <div class="pp-empty-text">上传一张参考图(含人物 + 背景)</div>
- <button class="pp-btn pp-btn-primary" type="button" (click)="benchmarkInput.click()" [disabled]="running">
- <svg class="pp-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
- <polyline points="17 8 12 3 7 8"/>
- <line x1="12" y1="3" x2="12" y2="15"/>
- </svg>
- 选择参考图
- </button>
- </div>
- <div class="pp-preview" *ngIf="benchmarkPreviewUrl">
- <img [src]="benchmarkPreviewUrl" [alt]="benchmarkFileName">
- <div class="pp-preview-meta">
- <div class="pp-preview-name">{{benchmarkFileName}}</div>
- <button class="pp-btn pp-btn-ghost" type="button" (click)="clearBenchmark()" [disabled]="running">
- <svg class="pp-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <polyline points="3 6 5 6 21 6"/>
- <path d="M19 6l-2 14a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L5 6"/>
- <path d="M10 11v6M14 11v6"/>
- </svg>
- 移除
- </button>
- </div>
- </div>
- </div>
- <!-- 生成按钮 + 进度 -->
- <div *ngIf="mode === 'benchmark'" class="pp-action">
- <button class="pp-btn pp-btn-primary" type="button"
- (click)="run()"
- [disabled]="running || !benchmarkFile">
- <svg class="pp-svg" [class.spinning]="running" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <ng-container *ngIf="!running">
- <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
- </ng-container>
- <ng-container *ngIf="running">
- <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
- </ng-container>
- </svg>
- {{running ? '生成中...' : (generatedPortraitUrl ? '重新生成' : '生成形象图')}}
- </button>
- <button class="pp-btn pp-btn-ghost" type="button" *ngIf="generatedPortraitUrl && !running" (click)="reset()">
- <svg class="pp-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <polyline points="23 4 23 10 17 10"/>
- <polyline points="1 20 1 14 7 14"/>
- <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
- </svg>
- 清除结果
- </button>
- <span class="pp-hint">jimeng-img-v4 + Gemini</span>
- </div>
- <div class="pp-status" *ngIf="running || statusText">
- <svg class="pp-svg" [class.spinning]="running" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
- [style.color]="running ? 'var(--accent-color, #3b82f6)' : 'var(--success-color, #10b981)'">
- <ng-container *ngIf="running">
- <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
- </ng-container>
- <ng-container *ngIf="!running">
- <circle cx="12" cy="12" r="10"/>
- <polyline points="9 12 11 14 15 10"/>
- </ng-container>
- </svg>
- <div class="pp-status-body">
- <div class="pp-status-text">{{statusText}}</div>
- <div class="pp-status-bar" [class.pp-status-bar--indeterminate]="running && progress < 5"><div class="pp-status-bar__fill" [style.width]="progress + '%'"></div></div>
- </div>
- <div class="pp-status-pct">{{progress}}%</div>
- </div>
- <div class="pp-error" *ngIf="errorText">{{errorText}}</div>
- <!-- 生成结果对比预览 -->
- <div class="pp-result" *ngIf="generatedPortraitUrl">
- <div class="pp-result-row">
- <div class="pp-result-cell">
- <div class="pp-result-cap">原图</div>
- <img [src]="selfiePreviewUrl" alt="原图">
- </div>
- <div class="pp-result-cell" *ngIf="benchmarkPreviewUrl">
- <div class="pp-result-cap">参考图(姿态/场景)</div>
- <img [src]="benchmarkPreviewUrl" alt="参考图">
- </div>
- <div class="pp-result-cell">
- <div class="pp-result-cap pp-result-cap-success">✨ 人物替换后形象图(将用于数字人)</div>
- <img [src]="generatedPortraitUrl" alt="生成形象图" class="pp-result-img-final">
- </div>
- </div>
- <div class="pp-summary" *ngIf="generatedSummary">
- <strong>参考图姿态提示词:</strong>{{generatedSummary}}
- </div>
- </div>
- </div>
- `,
- styles: [`
- :host { display: block; }
- .pp-container { display: flex; flex-direction: column; gap: 10px; }
- .pp-label { font-size: 13px; font-weight: 600; color: var(--text-primary, #e5e7eb); }
- .pp-label-sub { font-size: 12.5px; color: var(--text-secondary, #9ca3af); display: block; margin-bottom: 6px; }
- /* 模式卡片 */
- .pp-mode-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
- .pp-mode-card {
- padding: 10px 12px; border: 1px solid var(--border-color, rgba(255,255,255,0.1));
- border-radius: 10px; cursor: pointer; transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
- background: rgba(255,255,255,0.02);
- }
- .pp-mode-card:hover:not(.disabled) { border-color: var(--accent-color, #3b82f6); background: rgba(59,130,246,0.06); }
- .pp-mode-card.active {
- border-color: var(--accent-color, #3b82f6);
- background: linear-gradient(135deg, rgba(59,130,246,0.10), rgba(168,85,247,0.10));
- box-shadow: 0 0 0 1px var(--accent-color, #3b82f6) inset;
- }
- .pp-mode-card.disabled { opacity: 0.5; cursor: not-allowed; }
- .pp-mode-title { font-size: 13px; font-weight: 600; margin-bottom: 3px; color: var(--text-primary, #e5e7eb); }
- .pp-mode-desc { font-size: 11.5px; color: var(--text-muted, #9ca3af); line-height: 1.45; }
- /* 参考图上传 */
- .pp-benchmark { margin-top: 2px; }
- .pp-empty {
- padding: 14px; border: 1.5px dashed var(--border-color, rgba(255,255,255,0.15)); border-radius: 10px;
- display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center;
- background: rgba(255,255,255,0.015);
- }
- .pp-empty-text { font-size: 12px; color: var(--text-muted, #9ca3af); }
- .pp-preview {
- display: flex; gap: 10px; align-items: center; padding: 8px;
- background: rgba(255,255,255,0.04); border-radius: 10px;
- border: 1px solid var(--border-color, rgba(255,255,255,0.08));
- }
- .pp-preview img { width: 56px; height: 56px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
- .pp-preview-meta { flex: 1; display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; }
- .pp-preview-name {
- font-size: 12px; font-weight: 500; color: var(--text-primary, #e5e7eb);
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
- }
- /* 按钮 —— 使用 button.xxx 提升具体性,避免被全局 .btn / button:hover 覆盖 */
- button.pp-btn {
- display: inline-flex; align-items: center; gap: 6px;
- padding: 7px 12px; border-radius: 8px; font-size: 12.5px; font-weight: 500;
- border: 1px solid var(--border-color, rgba(255,255,255,0.12));
- background: rgba(99, 102, 241, 0.10);
- color: var(--text-primary, #1f2937);
- cursor: pointer; transition: background 0.15s, box-shadow 0.15s, transform 0.15s, border-color 0.15s;
- white-space: nowrap;
- }
- button.pp-btn:hover:not(:disabled) {
- background: rgba(99, 102, 241, 0.18);
- border-color: rgba(99, 102, 241, 0.45);
- transform: translateY(-1px);
- }
- button.pp-btn:disabled { opacity: 0.5; cursor: not-allowed; }
- button.pp-btn-primary {
- background: linear-gradient(135deg, #3b82f6, #8b5cf6);
- border-color: transparent;
- color: #ffffff;
- box-shadow: 0 2px 8px rgba(59,130,246,0.3);
- }
- button.pp-btn-primary:hover:not(:disabled) {
- background: linear-gradient(135deg, #2563eb, #7c3aed);
- border-color: transparent;
- color: #ffffff;
- box-shadow: 0 4px 14px rgba(59,130,246,0.5);
- transform: translateY(-1px);
- }
- button.pp-btn-primary:active:not(:disabled) {
- background: linear-gradient(135deg, #1d4ed8, #6d28d9);
- transform: translateY(0);
- }
- button.pp-btn-ghost {
- background: transparent;
- border-color: var(--border-color, rgba(0,0,0,0.12));
- color: var(--text-primary, #1f2937);
- }
- button.pp-btn-ghost:hover:not(:disabled) {
- background: rgba(0,0,0,0.04);
- border-color: rgba(0,0,0,0.2);
- }
- /* 动作区 */
- .pp-action { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
- .pp-hint { font-size: 11px; color: var(--text-muted, #6b7280); margin-left: auto; }
- /* 状态条 */
- .pp-status {
- display: flex; align-items: center; gap: 10px; padding: 8px 10px;
- background: rgba(255,255,255,0.04); border-radius: 8px;
- border: 1px solid var(--border-color, rgba(255,255,255,0.08));
- }
- .pp-status-body { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
- .pp-status-text { font-size: 12px; color: var(--text-primary, #e5e7eb); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .pp-status-bar { position: relative; height: 4px; background: rgba(99,102,241,0.12); border-radius: 2px; overflow: hidden; }
- .pp-status-bar__fill {
- height: 100%;
- background: linear-gradient(90deg, #3b82f6, #8b5cf6);
- transition: width 0.3s ease;
- }
- /* 上传阶段(progress<5)显示流动条纹,避免视觉上静止 */
- .pp-status-bar--indeterminate::after {
- content: '';
- position: absolute; inset: 0;
- background: linear-gradient(90deg,
- transparent 0%,
- rgba(139, 92, 246, 0.6) 50%,
- transparent 100%);
- animation: pp-indeterminate 1.2s linear infinite;
- }
- @keyframes pp-indeterminate {
- 0% { transform: translateX(-100%); }
- 100% { transform: translateX(100%); }
- }
- .pp-status-pct { font-size: 11.5px; font-weight: 600; color: var(--accent-color, #3b82f6); flex-shrink: 0; }
- .pp-error {
- padding: 8px 10px; background: rgba(220, 38, 38, 0.08);
- color: #ef4444; border-radius: 8px; font-size: 12px;
- border: 1px solid rgba(220, 38, 38, 0.25);
- }
- /* 结果对比 */
- .pp-result {
- display: flex; flex-direction: column; gap: 8px; padding: 10px;
- background: rgba(255,255,255,0.03); border-radius: 10px;
- border: 1px solid var(--border-color, rgba(255,255,255,0.08));
- }
- .pp-result-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
- .pp-result-cell { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
- .pp-result-cap { font-size: 11px; color: var(--text-muted, #9ca3af); }
- .pp-result-cap-success { color: #10b981; font-weight: 600; }
- .pp-result-cell img {
- width: 100%; aspect-ratio: 4/5; object-fit: cover;
- border-radius: 6px; background: rgba(0,0,0,0.2);
- }
- .pp-result-img-final { box-shadow: 0 0 0 2px #10b981; }
- .pp-summary {
- font-size: 11.5px; color: var(--text-muted, #9ca3af); padding: 8px 10px;
- background: rgba(0,0,0,0.15); border-radius: 6px; line-height: 1.5;
- }
- .pp-summary strong { color: var(--text-primary, #e5e7eb); }
- /* SVG 图标 */
- .pp-svg { width: 14px; height: 14px; flex-shrink: 0; }
- .pp-svg-lg { width: 28px; height: 28px; color: var(--text-muted, #6b7280); }
- .pp-svg.spinning { animation: pp-spin 1s linear infinite; }
- @keyframes pp-spin { to { transform: rotate(360deg); } }
- /* 窄列适配(在 dh-step-col 280px 列宽下) */
- @media (max-width: 1280px) {
- .pp-mode-grid { grid-template-columns: 1fr; }
- .pp-result-row { grid-template-columns: 1fr; }
- .pp-result-cell img { aspect-ratio: auto; max-height: 180px; }
- }
- `]
- })
- export class PortraitPrepareComponent implements OnDestroy {
- /** 用户上传的自拍/形象图 File(必填) */
- @Input() selfieFile: File | null = null;
- /** 自拍预览 URL(用于对比展示) */
- @Input() selfiePreviewUrl: string = '';
- /** 当模式或生成结果变化时通知父组件 */
- @Output() prepared = new EventEmitter<PortraitPrepareOutput>();
- mode: PortraitPrepareMode = 'raw';
- benchmarkFile: File | null = null;
- benchmarkFileName: string = '';
- benchmarkPreviewUrl: string = '';
- running: boolean = false;
- statusText: string = '';
- progress: number = 0;
- errorText: string = '';
- generatedPortraitUrl: string = '';
- generatedFullBodyUrl: string = '';
- generatedSummary: string = '';
- // 已上传过的 selfie/benchmark URL,避免重复上传
- private cachedSelfieUrl: string = '';
- private cachedBenchmarkUrl: string = '';
- private cachedSelfieFile: File | null = null;
- private cachedBenchmarkFile: File | null = null;
- constructor(
- private jimeng: JimengService,
- private portraitPipeline: PortraitPipelineService
- ) {}
- ngOnDestroy(): void {
- this.revokeBenchmarkPreview();
- }
- setMode(mode: PortraitPrepareMode): void {
- if (this.running) return;
- this.mode = mode;
- this.generatedPortraitUrl = '';
- this.generatedFullBodyUrl = '';
- this.generatedSummary = '';
- this.statusText = '';
- this.progress = 0;
- this.errorText = '';
- if (mode !== 'benchmark') {
- this.clearBenchmark();
- }
- this.emitState();
- }
- onBenchmarkSelected(event: Event): void {
- const input = event.target as HTMLInputElement;
- if (!input.files || input.files.length === 0) return;
- const file = input.files[0];
- const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif', 'image/webp'];
- const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
- const allowedExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
- if ((!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) || file.size <= 0) {
- this.errorText = '对标场景图仅支持 jpg/jpeg/png/gif/webp 格式';
- input.value = '';
- return;
- }
- if (file.size > 20 * 1024 * 1024) {
- this.errorText = '对标场景图大小不能超过 20MB';
- input.value = '';
- return;
- }
- this.revokeBenchmarkPreview();
- this.benchmarkFile = file;
- this.benchmarkFileName = file.name;
- this.benchmarkPreviewUrl = URL.createObjectURL(file);
- this.errorText = '';
- this.generatedPortraitUrl = '';
- this.cachedBenchmarkUrl = '';
- this.cachedBenchmarkFile = null;
- input.value = '';
- this.emitState();
- }
- clearBenchmark(): void {
- this.revokeBenchmarkPreview();
- this.benchmarkFile = null;
- this.benchmarkFileName = '';
- this.cachedBenchmarkUrl = '';
- this.cachedBenchmarkFile = null;
- }
- reset(): void {
- if (this.running) return;
- this.generatedPortraitUrl = '';
- this.generatedFullBodyUrl = '';
- this.generatedSummary = '';
- this.statusText = '';
- this.progress = 0;
- this.errorText = '';
- this.emitState();
- }
- run(): void {
- if (this.running) return;
- if (!this.selfieFile) {
- this.errorText = '请先上传人物自拍/形象图';
- return;
- }
- if (this.mode === 'benchmark' && !this.benchmarkFile) {
- this.errorText = '请上传一张对标账号的场景图';
- return;
- }
- this.running = true;
- this.errorText = '';
- this.progress = 1;
- this.statusText = '正在上传形象图...';
- // Step 1:上传自拍(带缓存)
- this.ensureUrl(this.selfieFile, this.cachedSelfieFile, this.cachedSelfieUrl, 'portrait-selfie').then((selfieUrl) => {
- this.cachedSelfieFile = this.selfieFile;
- this.cachedSelfieUrl = selfieUrl;
- this.progress = 2;
- this.statusText = this.benchmarkFile ? '正在上传参考图...' : '形象图上传完成';
- // Step 2:上传对标图(带缓存,benchmark 模式才需要)
- const benchmarkPromise: Promise<string | null> = this.benchmarkFile
- ? this.ensureUrl(this.benchmarkFile, this.cachedBenchmarkFile, this.cachedBenchmarkUrl, 'portrait-benchmark').then((url) => {
- this.cachedBenchmarkFile = this.benchmarkFile;
- this.cachedBenchmarkUrl = url;
- this.progress = 4;
- this.statusText = '素材上传完成,准备分析...';
- return url;
- })
- : Promise.resolve(null);
- return benchmarkPromise.then((benchmarkUrl) => ({ selfieUrl, benchmarkUrl }));
- }).then(({ selfieUrl, benchmarkUrl }) => {
- // Step 3:执行 pipeline
- this.portraitPipeline.buildBenchmarkedPortrait(selfieUrl, benchmarkUrl, (status, progress) => {
- this.statusText = status;
- this.progress = Math.round(progress);
- }).subscribe({
- next: (result: PortraitPipelineResult) => {
- this.generatedFullBodyUrl = result.fullBodyUrl;
- this.generatedPortraitUrl = result.finalPortraitUrl;
- this.generatedSummary = result.benchmarkPrompt?.merged || '';
- this.running = false;
- this.statusText = '形象图生成完成';
- this.progress = 100;
- this.emitState();
- },
- error: (err) => {
- this.running = false;
- this.errorText = this.getErrorText(err, '形象图生成失败');
- this.emitState();
- }
- });
- }).catch((err) => {
- this.running = false;
- this.errorText = this.getErrorText(err, '素材上传失败');
- this.emitState();
- });
- }
- private ensureUrl(file: File, cachedFile: File | null, cachedUrl: string, prefix: string): Promise<string> {
- if (cachedUrl && cachedFile === file) {
- return Promise.resolve(cachedUrl);
- }
- return new Promise<string>((resolve, reject) => {
- this.jimeng.uploadFileToParse(
- file,
- `${prefix}-${Date.now()}-${file.name}`,
- file.type || 'image/jpeg'
- ).subscribe({
- next: (url: string) => resolve(url),
- error: (err) => reject(err)
- });
- });
- }
- private emitState(): void {
- this.prepared.emit({
- portraitUrl: this.generatedPortraitUrl,
- fullBodyUrl: this.generatedFullBodyUrl,
- benchmarkSummary: this.generatedSummary,
- mode: this.mode
- });
- }
- private revokeBenchmarkPreview(): void {
- if (this.benchmarkPreviewUrl) {
- URL.revokeObjectURL(this.benchmarkPreviewUrl);
- this.benchmarkPreviewUrl = '';
- }
- }
- private getErrorText(err: any, fallback: string): string {
- return err?.error?.message || err?.error?.error || err?.message || fallback;
- }
- }
|