Files
omniai-web/src/features/workbench/workbenchReferenceUtils.ts
T

215 lines
7.1 KiB
TypeScript
Raw Normal View History

import {
REFERENCE_IMAGE_COMPRESS_THRESHOLD,
REFERENCE_IMAGE_MAX_DIMENSION,
REFERENCE_IMAGE_INITIAL_QUALITY,
REFERENCE_IMAGE_MIN_QUALITY,
type WorkbenchMode,
type ReferenceKind,
type ReferenceItem,
} from "./workbenchConstants";
import { resolvePreUploadedUrl } from "../../api/referenceUploadService";
export function getRatioOptionClassName(value: string) {
return `ai-workbench-ratio-option__preview--${value.replace(":", "-")}`;
}
export function getSettingsGridColumnsClassName(columns: 2 | 3 | 4 = 3) {
return `ai-workbench-settings-panel__grid--cols-${columns}`;
}
export function getReferenceAccept(mode: WorkbenchMode, videoFrameMode?: string) {
if (mode === "chat") return ".docx,.txt,.md,.xlsx,.xls,.png,.jpg,.jpeg,.gif,.webp";
if (mode === "image") return "image/*";
if (videoFrameMode === "start-end") return "image/*";
return "image/*,video/mp4,video/quicktime,video/webm,video/x-msvideo,.mp4,.mov,.webm,.avi,audio/mpeg,audio/mp3,audio/wav,audio/x-wav,.mp3,.wav";
}
export function getReferenceUploadLabel(mode: WorkbenchMode) {
if (mode === "video") return "参考内容";
if (mode === "image") return "参考图";
return "附件";
}
export function getReferenceLimit(mode: WorkbenchMode, videoFrameMode?: string) {
if (mode === "video" && videoFrameMode === "start-end") return 2;
if (mode === "video") return 12;
if (mode === "image") return 9;
return 4;
}
export function getReferenceKindLabel(kind: ReferenceKind) {
if (kind === "image") return "图片";
if (kind === "video") return "视频";
if (kind === "audio") return "音频";
return "附件";
}
export function getReferenceEmptyCopy(mode: WorkbenchMode) {
if (mode === "video") return "上传最多12个参考素材,首尾帧模式仅保留2张图片,输入文字或 @ 引用内容,自由组合图、文、音、视频多元素";
if (mode === "image") return "最多上传9张参考图,输入文字或 @ 引用内容,控制角色、风格和构图";
return "上传附件后可用 @ 引用,帮助 Agent 读取上下文";
}
export function hexToRgbTriplet(hex: string) {
const normalized = hex.replace("#", "");
const full = normalized.length === 3
? normalized
.split("")
.map((char) => `${char}${char}`)
.join("")
: normalized;
const value = Number.parseInt(full, 16);
const r = (value >> 16) & 255;
const g = (value >> 8) & 255;
const b = value & 255;
return `${r}, ${g}, ${b}`;
}
export function inferReferenceKind(file: File, mode: WorkbenchMode): ReferenceKind {
if (file.type.startsWith("image/")) return "image";
if (file.type.startsWith("video/")) return "video";
if (file.type.startsWith("audio/")) return "audio";
return mode === "chat" ? "file" : "image";
}
export function disposeReferencePreview(item: Pick<ReferenceItem, "previewUrl">) {
if (item.previewUrl) URL.revokeObjectURL(item.previewUrl);
}
export function fileToDataUrl(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
2026-06-09 12:02:30 +08:00
if (typeof reader.result === "string") {
resolve(reader.result);
} else {
reject(new Error("Unable to read reference file"));
}
};
reader.onerror = () => reject(reader.error || new Error("Unable to read reference file"));
reader.readAsDataURL(file);
});
}
export function bytesToHex(buffer: ArrayBuffer) {
return Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
export async function buildReferenceFingerprint(file: File, kind: ReferenceKind) {
if (kind === "image" && window.crypto?.subtle) {
const digest = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer());
return `image:${bytesToHex(digest)}`;
}
return `${kind}:${file.name}:${file.size}:${file.lastModified}:${file.type}`;
}
export function canCompressReferenceImage(file: File) {
return (
file.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD &&
file.type.startsWith("image/") &&
!/svg|gif/i.test(file.type)
);
}
function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number) {
return new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, type, quality);
});
}
function getCompressedImageName(fileName: string) {
const baseName = fileName.replace(/\.[^.]+$/, "");
return `${baseName || "reference"}.jpg`;
}
export async function compressReferenceImageIfNeeded(file: File) {
if (!canCompressReferenceImage(file)) {
return { file, compressed: false };
}
try {
const bitmap = await createImageBitmap(file);
const scale = Math.min(1, REFERENCE_IMAGE_MAX_DIMENSION / Math.max(bitmap.width, bitmap.height));
let width = Math.max(1, Math.round(bitmap.width * scale));
let height = Math.max(1, Math.round(bitmap.height * scale));
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) {
bitmap.close();
return { file, compressed: false };
}
const render = () => {
canvas.width = width;
canvas.height = height;
context.fillStyle = "#ffffff";
context.fillRect(0, 0, width, height);
context.drawImage(bitmap, 0, 0, width, height);
};
const encode = async () => {
let quality = REFERENCE_IMAGE_INITIAL_QUALITY;
let nextBlob = await canvasToBlob(canvas, "image/jpeg", quality);
while (nextBlob && nextBlob.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD && quality > REFERENCE_IMAGE_MIN_QUALITY) {
quality = Math.max(REFERENCE_IMAGE_MIN_QUALITY, quality - 0.08);
nextBlob = await canvasToBlob(canvas, "image/jpeg", quality);
}
return nextBlob;
};
render();
let blob = await encode();
while (blob && blob.size > REFERENCE_IMAGE_COMPRESS_THRESHOLD && Math.max(width, height) > 960) {
width = Math.max(1, Math.round(width * 0.82));
height = Math.max(1, Math.round(height * 0.82));
render();
blob = await encode();
}
bitmap.close();
if (!blob || blob.size >= file.size) {
return { file, compressed: false };
}
return {
file: new File([blob], getCompressedImageName(file.name), {
type: "image/jpeg",
lastModified: file.lastModified,
}),
compressed: true,
};
} catch {
return { file, compressed: false };
}
}
export function buildReferenceToken(kind: ReferenceKind, index: number) {
if (kind === "image") return `@图片${index}`;
if (kind === "video") return `@视频${index}`;
if (kind === "audio") return `@音频${index}`;
return `@附件${index}`;
}
export async function resolveReferenceUrls(items: ReferenceItem[]): Promise<string[]> {
const tasks = items.map(async (item) => {
if (item.remoteUrl) return item.remoteUrl;
if (!item.file) {
if (item.previewUrl && /^https?:\/\//i.test(item.previewUrl)) {
return item.previewUrl;
}
return null;
}
const url = await resolvePreUploadedUrl(item.file, item.name, item.fingerprint);
if (url) {
item.remoteUrl = url;
return url;
}
return null;
});
const results = await Promise.all(tasks);
return results.filter((url): url is string => url !== null);
2026-06-09 12:02:30 +08:00
}