ChatGPT Image 2.0生图工具核心JS实现

0 阅读4分钟

这篇文章只讲本项目里“ChatGPT Image 2.0生图”工具的功能 JS 实现。工具用 Vue 管理页面状态,核心链路可以概括为:

选择模式 -> 组装参数 -> 处理图片输入 -> 发起请求 -> 解析图片结果 -> 保存历史记录

它不只是一个文本生图表单,还支持参考图生图、图片编辑、变体生成、遮罩编辑、流式预览和本地历史记录。

在线工具网址:see-tool.com/chatgpt-ima…
工具截图:
工具截图.png

1)用 mode 区分不同生图流程

工具内部用 mode 控制当前操作类型,默认是 generate。不同模式最终会走不同的参数构造逻辑:

  • generate:纯文本生图
  • image:带参考图的生图
  • edit:上传图片后按提示词编辑
  • variation:基于 PNG 方图生成变体

提交时先根据模式构造 payload,再决定请求入口:

submit: function () {
  var payload;
  var endpoint;
  if (this.mode === "generate") {
    payload = this.generationPayload();
    endpoint = "/api/chatgpt-image-2/generate";
  } else if (this.mode === "variation") {
    payload = this.variationPayload();
    endpoint = "/api/chatgpt-image-2/variation";
  } else {
    payload = this.editPayload();
    endpoint = "/api/chatgpt-image-2/edit";
  }
  this.pendingPayload = payload;
  this.pendingEndpoint = endpoint;
}

这样做的好处是页面交互可以共用一套状态,但参数校验和接口调用仍然按模式拆开。

2)公共参数统一由 commonPayload 生成

生图、编辑、参考图模式都有一批公共字段,例如模型、提示词、数量、尺寸、质量、背景、输出格式、压缩比例等。实现里用 commonPayload 集中处理这些字段。

commonPayload: function () {
  var outputFormat = this.format;
  var payload = cleanObject({
    model: "gpt-image-2",
    prompt: String(this.prompt || "").trim(),
    n: this.parseInteger(this.count, 1, 1, 10),
    size: this.size,
    quality: this.quality,
    background: this.background,
    output_format: outputFormat,
    moderation: this.moderation,
    user: String(this.userId || "").trim(),
    stream: this.stream && this.mode !== "variation" ? true : undefined,
    partial_images: this.stream && this.mode !== "variation" ? this.parseInteger(this.partials, 2, 0, 3) : undefined,
  });

  if (outputFormat === "jpeg" || outputFormat === "webp") {
    payload.output_compression = this.parseInteger(this.compression, 100, 0, 100);
  }
  return Object.assign({}, payload, this.parseExtraJson());
}

这里的 cleanObject 会移除空字符串、nullundefined,避免把无效字段传给后端。parseInteger 则负责把用户输入转成安全范围内的数字。

3)图片输入先转成统一结构

参考图、编辑图、遮罩图和变体图都来自文件输入。工具没有直接保存原始 File,而是用 FileReader 转成 Data URL,并额外读取图片宽高。

fileToDataUrl: function (file) {
  return new Promise(function (resolve, reject) {
    var reader = new FileReader();
    reader.onload = function () {
      var dataUrl = reader.result;
      var image = new Image();
      image.onload = function () {
        resolve({
          id: createId(),
          name: file.name,
          type: file.type || "application/octet-stream",
          size: file.size,
          width: image.naturalWidth,
          height: image.naturalHeight,
          dataUrl: dataUrl,
        });
      };
      image.src = dataUrl;
    };
    reader.onerror = function () {
      reject(reader.error);
    };
    reader.readAsDataURL(file);
  });
}

转换后的图片对象结构稳定,后续无论是展示缩略图、提交参数,还是做变体图尺寸校验,都可以复用。

4)编辑模式的 payload 会合并图片和遮罩

编辑模式要求有提示词和图片输入。图片来源分两种:本地上传的图片,或者用户输入的远程图片引用。实现里明确禁止两种来源混用:

apiSafeImageList: function (uploads, refs) {
  if (uploads.length && refs.length) {
    throw new Error(this.t("chatgptImage2Generator.messages.mixedImages"));
  }
  return uploads.length ? uploads : refs;
}

编辑 payload 会在公共参数基础上追加 images,如果用户启用了遮罩,则再追加 mask

editPayload: function () {
  var payload = this.commonPayload();
  var uploads = this.mode === "image" ? this.imageInputs : this.editInputs;
  var refs = this.mode === "image" ? this.imageReferencesFrom(this.imageRefs) : this.imageReferencesFrom(this.editRefs);
  var mask = this.imageReference(this.maskRef) || this.selectedMask();

  if (!payload.prompt) throw new Error(this.t("chatgptImage2Generator.messages.promptRequired"));
  payload.images = this.apiSafeImageList(uploads, refs);
  if (!payload.images.length) throw new Error(this.t("chatgptImage2Generator.messages.imageRequired"));
  if (this.inputFidelity) payload.input_fidelity = this.inputFidelity;
  if (this.mode === "edit" && (this.useMask || this.maskRef.trim()) && mask) {
    payload.mask = mask;
  }
  return payload;
}

5)遮罩绘制用 Canvas 生成透明区域

编辑模式支持在图片上涂抹遮罩。实现上准备了一个隐藏绘制层 maskPaintCanvas,用户在可见画布上拖动时,会把笔刷圆形画到绘制层。

生成遮罩时,会先创建一张白色画布,再把涂抹过的位置改成透明,最后导出 PNG Data URL:

buildMaskDataUrl: function () {
  var source = this.maskPaintCanvas;
  var mask = document.createElement("canvas");
  mask.width = source.width;
  mask.height = source.height;
  var maskCtx = mask.getContext("2d");
  maskCtx.fillStyle = "#ffffff";
  maskCtx.fillRect(0, 0, mask.width, mask.height);
  var paint = source.getContext("2d").getImageData(0, 0, source.width, source.height);
  var output = maskCtx.getImageData(0, 0, mask.width, mask.height);
  for (var i = 0; i < paint.data.length; i += 4) {
    if (paint.data[i + 3] > 0) output.data[i + 3] = 0;
  }
  maskCtx.putImageData(output, 0, 0);
  return mask.toDataURL("image/png");
}

这段逻辑的关键点是:用户看到的是红色涂抹层,真正提交的是白底透明区域的 PNG 遮罩。

6)普通响应和流式响应分开解析

非流式响应返回完整 JSON,工具会把 b64_jsonurl 统一整理成图片对象:

normalizeImages: function (response, payload) {
  var format = response.output_format || payload.output_format || "png";
  var data = Array.isArray(response.data) ? response.data : [];
  return data
    .map(function (item, index) {
      var src = item.b64_json ? dataUrlFromB64(item.b64_json, format) : item.url;
      if (!src) return null;
      return {
        id: createId(),
        src: src,
        format: format,
        revisedPrompt: item.revised_prompt || "",
        index: index,
      };
    })
    .filter(Boolean);
}

流式响应则通过 ReadableStream 逐段读取,再按 SSE 的空行边界拆块:

parseSseBlocks: function (buffer) {
  var blocks = [];
  var boundary = buffer.indexOf("\n\n");
  while (boundary !== -1) {
    blocks.push(buffer.slice(0, boundary));
    buffer = buffer.slice(boundary + 2);
    boundary = buffer.indexOf("\n\n");
  }
  return { blocks: blocks, rest: buffer };
}

解析出来的数据如果是中间图,就放入 partialImages;如果是完成图,就放入 resultImages

7)本地历史记录使用 IndexedDB

生成完成后,工具会把结果写入 IndexedDB。记录里包含生成时间、模式、提示词、模型、图片数组和用量信息。

saveResultRecord: async function (images, payload, mode, response) {
  await this.saveHistoryRecord({
    id: createId(),
    createdAt: Date.now(),
    mode: mode,
    prompt: payload.prompt || "",
    model: payload.model || (mode === "variation" ? "dall-e-2" : "gpt-image-2"),
    images: images,
    usage: response.usage || null,
  });
}

历史记录按时间倒序读取,并限制最多保留 60 条。这样用户可以恢复之前的生成结果,同时不会让本地记录无限增长。

8)核心实现总结

这个工具的核心 JS 并不复杂,关键在于把多种图片生成场景拆成清晰的数据流:公共参数统一生成,图片输入统一转换,编辑模式独立处理遮罩,结果解析统一转成前端可展示的图片对象。Vue 只负责把这些状态串起来,真正的功能逻辑都围绕 payload 构造、图片转换、响应解析和历史记录展开。