如何实现chatgpt的打字机效果

这篇具有很好参考价值的文章主要介绍了如何实现chatgpt的打字机效果。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

点击↑上方↑蓝色“编了个程”关注我~

如何实现chatgpt的打字机效果

这是Yasin的第 88 篇原创文章

如何实现chatgpt的打字机效果

打字机效果

最近在搭建chat gpt代理的时候,发现自己的配置虽然能够调通接口,返回数据,但是结果是一次性显示出来的,不像之前的chat gpt的官网demo那样实现了打字机效果,一个字一个字出来。

如何实现chatgpt的打字机效果

所以研究了一下chat gpt打字机效果的原理,后续如果要实现类似的效果可以借鉴。

纯前端实现打字机效果

最开始我搜索打字机效果时,出现的结果大多数是纯前端的方案。其原理也很简单,通过js定时把内容输出到屏幕。下面是chat gpt的答案:

前端实现打字机效果可以通过以下步骤:

  1. 将文本内容嵌入到 HTML 元素中,如 div 或 span

  2. 通过 CSS 样式设置元素的显示方式为隐藏(如 display: none;)。

  3. 使用 JavaScript 获取该元素,并逐个显示其中的字符。

  4. 使用定时器(如 setInterval() 函数)控制每个字符的出现时间间隔,从而实现逐个逐个显示的效果。

  5. 当所有字符都被显示后,停止定时器以避免不必要的计算开销。

下面是一个简单的示例代码:

HTML:

<div id="typewriter">Hello World!</div>

CSS:

#typewriter {
  display: none;
}

JS:

const element = document.getElementById('typewriter');

let i = 0;
const interval = setInterval(() => {
  element.style.display = 'inline';
  element.textContent = element.textContent.slice(0, i++) + '_';

  if (i > element.textContent.length) {
    clearInterval(interval);
    element.textContent = element.textContent.slice(0, -1);
  }
}, 100);

该代码会将 idtypewriter 的元素中的文本逐个显示,每个字符之间相隔 100 毫秒。最终显示完毕后,会将最后一个字符的下划线去除。

流式输出

后面我抓包以及查看了chat gpt的官方文档之后,发现事情并没有这么简单。chat gpt的打字机效果并不是后端一次性返回后,纯前端的样式。而是后端通过流式输出不断向前端输出内容。

在chat gpt官方文档中,有一个参数可以让它实现流式输出:

如何实现chatgpt的打字机效果

这是一个叫“event_stream_format”的协议规范。

event_stream_format(简称 ESF)是一种基于 HTTP/1.1 的、用于实现服务器推送事件的协议规范。它定义了一种数据格式,可以将事件作为文本流发送给客户端。ESF 的设计目标是提供简单有效的实时通信方式,以及支持众多平台和编程语言。

ESF 数据由多行文本组成,每行用 \n(LF)分隔。其中,每个事件由以下三部分组成:

  • 事件类型(event)

  • 数据(data)

  • 标识符(id)

例如:

event: message 
data: Hello, world! 
id: 123

这个例子表示一个名为 message 的事件,携带着消息内容 Hello, world!,并提供了一个标识符为 123 的可选参数。

ESF 还支持以下两种特殊事件类型:

  • 注释(comment):以冒号开头的一行,只做为注释使用。

  • 重传(retry):指定客户端重连的时间间隔,以毫秒为单位。

例如:

: This is a comment

retry: 10000

event: update
data: {"status": "OK"}

ESF 协议还支持 Last-Event-ID 头部,它允许客户端在断线后重新连接,并从上次连接中断处恢复。当客户端连接时,可以通过该头部将上次最新的事件 ID 传递给服务器,以便服务器根据该 ID 继续发送事件。

ESF 是一种简单的、轻量级的协议,适用于需要实时数据交换和多方通信的场景。由于其使用了标准的 HTTP/1.1 协议,因此可以轻易地在现有的 Web 基础设施上实现。

抓包可以发现这个响应长这样:如何实现chatgpt的打字机效果

如何实现chatgpt的打字机效果

可以看到是data: 加上一个json,每次的流式数据在delta里面。

http response中有几个重要的头:如何实现chatgpt的打字机效果

其中,keep-alive是保持客户端和服务端的双向通信,这个大家应该都比较了解。下面解释一下另外两个头.

这里其实openai返回的是text/event-streamtext/event-stream 是一种流媒体协议,用于在 Web 应用程序中推送实时事件。它的内容是文本格式的,每个事件由一个或多个字段组成,以换行符(\n)分隔。这个 MIME 类型通常用于服务器到客户端的单向通信,例如服务器推送最新的新闻、股票报价等信息给客户端。

我这里使用的开源项目chatgpt-web抓的包,请求被nodejs包了一层,返回了application/octet-stream (不太清楚这么做的动机是什么),它是一种 MIME 类型,通常用于指示某个资源的内容类型为二进制文件,也就是未知的二进制数据流。该类型通常不会执行任何自定义处理,并且可以由客户端根据需要进行下载或保存。

Transfer-Encoding: chunked 是一种 HTTP 报文传输编码方式,用于指示报文主体被分为多个等大小的块(chunks)进行传输。每个块包含一个十六进制数字的长度字段,后跟一个 CRLF(回车换行符),然后是实际的数据内容,最后以另一个 CRLF 结束。

使用 chunked 编码方式可以使服务器在发送未知大小的数据时更加灵活,同时也可以避免一些限制整个响应主体大小的限制。当接收端收到所有块后,会将它们组合起来,解压缩(如果需要),并形成原始的响应主体。

总之,Transfer-Encoding: chunked 允许服务器在发送 HTTP 响应时,动态地生成报文主体,而不必事先确定其大小,从而提高了通信效率和灵活性。

服务端的实现

作为chat gpt代理

如果写一个golang http服务作为chat gpt的代理,只需要循环扫描chat gpt返回的每行结果,每行作为一个事件输出给前端就行了。核心代码如下:

// 设置Content-Type标头为text/event-stream  
w.Header().Set("Content-Type", "text/event-stream")  
// 设置缓存控制标头以禁用缓存  
w.Header().Set("Cache-Control", "no-cache")  
w.Header().Set("Connection", "keep-alive")  
w.Header().Set("Keep-Alive", "timeout=5")  
// 循环读取响应体并将每行作为一个事件发送到客户端  
scanner := bufio.NewScanner(resp.Body)  
for scanner.Scan() {  
   eventData := scanner.Text()  
   if eventData == "" {  
      continue  
   }  
   fmt.Fprintf(w, "%s\n\n", eventData)  
   flusher, ok := w.(http.Flusher)  
   if ok {  
      flusher.Flush()  
   } else {  
      log.Println("Flushing not supported")  
   }  
}

自己作为服务端

这里模仿openai的数据结构,自己作为服务端,返回流式输出:

const Text = `  
proxy_cache:通过这个模块,Nginx 可以缓存代理服务器从后端服务器请求到的响应数据。当下一个客户端请求相同的资源时,Nginx 可以直接从缓存中返回响应,而不必去请求后端服务器。这大大降低了代理服务器的负载,同时也能提高客户端访问速度。需要注意的是,使用 proxy_cache 模块时需要谨慎配置缓存策略,避免出现缓存不一致或者过期的情况。  
  
proxy_buffering:通过这个模块,Nginx 可以将后端服务器响应数据缓冲起来,并在完整的响应数据到达之后再将其发送给客户端。这种方式可以减少代理服务器和客户端之间的网络连接数,提高并发处理能力,同时也可以防止后端服务器过早关闭连接,导致客户端无法接收到完整的响应数据。  
  
综上所述, proxy_cache 和 proxy_buffering 都可以通过缓存技术提高代理服务器性能和安全性,但需要注意合理的配置和使用,以避免潜在的缓存不一致或者过期等问题。同时, proxy_buffering 还可以通过缓冲响应数据来提高代理服务器的并发处理能力,从而更好地服务于客户端。  
`  
  
type ChatCompletionChunk struct {  
   ID      string `json:"id"`  
   Object  string `json:"object"`  
   Created int64  `json:"created"`  
   Model   string `json:"model"`  
   Choices []struct {  
      Delta struct {  
         Content string `json:"content"`  
      } `json:"delta"`  
      Index        int     `json:"index"`  
      FinishReason *string `json:"finish_reason"`  
   } `json:"choices"`  
}  
  
func handleSelfRequest(w http.ResponseWriter, r *http.Request) {  
   // 设置Content-Type标头为text/event-stream  
   w.Header().Set("Content-Type", "text/event-stream")  
   // 设置缓存控制标头以禁用缓存  
   w.Header().Set("Cache-Control", "no-cache")  
   w.Header().Set("Connection", "keep-alive")  
   w.Header().Set("Keep-Alive", "timeout=5")  
   w.Header().Set("Transfer-Encoding", "chunked")  
   // 生成一个uuid  
   uid := uuid.NewString()  
   created := time.Now().Unix()  
  
   for i, v := range Text {  
      eventData := fmt.Sprintf("%c", v)  
      if eventData == "" {  
         continue  
      }  
      var finishReason *string  
      if i == len(Text)-1 {  
         temp := "stop"  
         finishReason = &temp  
      }  
      chunk := ChatCompletionChunk{  
         ID:      uid,  
         Object:  "chat.completion.chunk",  
         Created: created,  
         Model:   "gpt-3.5-turbo-0301",  
         Choices: []struct {  
            Delta struct {  
               Content string `json:"content"`  
            } `json:"delta"`  
            Index        int     `json:"index"`  
            FinishReason *string `json:"finish_reason"`  
         }{  
            {               Delta: struct {  
                  Content string `json:"content"`  
               }{  
                  Content: eventData,  
               },  
               Index:        0,  
               FinishReason: finishReason,  
            },  
         },  
      }  
  
      fmt.Println("输出:" + eventData)  
      marshal, err := json.Marshal(chunk)  
      if err != nil {  
         return  
      }  
  
      fmt.Fprintf(w, "data: %v\n\n", string(marshal))  
      flusher, ok := w.(http.Flusher)  
      if ok {  
         flusher.Flush()  
      } else {  
         log.Println("Flushing not supported")  
      }  
      if i == len(Text)-1 {  
         fmt.Fprintf(w, "data: [DONE]")  
         flusher, ok := w.(http.Flusher)  
         if ok {  
            flusher.Flush()  
         } else {  
            log.Println("Flushing not supported")  
         }  
      }      time.Sleep(100 * time.Millisecond)  
   }  
}

核心是每次写进一行数据data: xx \n\n,最终以data: [DONE]结尾。

前端的实现

前端代码参考https://github.com/Chanzhaoyu/chatgpt-web的实现。

这里核心是使用了axios的onDownloadProgress钩子,当stream有输出时,获取chunk内容,更新到前端显示。

await fetchChatAPIProcess<Chat.ConversationResponse>({  
  prompt: message,  
  options,  
  signal: controller.signal,  
  onDownloadProgress: ({ event }) => {  
    const xhr = event.target  
    const { responseText } = xhr  
    // Always process the final line  
    const lastIndex = responseText.lastIndexOf('\n')  
    let chunk = responseText  
    if (lastIndex !== -1)  
      chunk = responseText.substring(lastIndex)  
    try {  
      const data = JSON.parse(chunk)  
      updateChat(  
        +uuid,  
        dataSources.value.length - 1,  
        {  
          dateTime: new Date().toLocaleString(),  
          text: lastText + data.text ?? '',  
          inversion: false,  
          error: false,  
          loading: false,  
          conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },  
          requestOptions: { prompt: message, options: { ...options } },  
        },  
      )  
  
      if (openLongReply && data.detail.choices[0].finish_reason === 'length') {  
        options.parentMessageId = data.id  
        lastText = data.text  
        message = ''  
        return fetchChatAPIOnce()  
      }  
  
      scrollToBottom()  
    }  
    catch (error) {  
    //  
    }  
  },  
})

在底层的请求代码中,设置对应的header和参数,监听data内容,回调onProgress函数。

const responseP = new Promise((resolve, reject) => {  
  const url = this._apiReverseProxyUrl;  
  const headers = {  
    ...this._headers,  
    Authorization: `Bearer ${this._accessToken}`,  
    Accept: "text/event-stream",  
    "Content-Type": "application/json"  
  };  
  if (this._debug) {  
    console.log("POST", url, { body, headers });  
  }  
  fetchSSE(  
    url,  
    {  
      method: "POST",  
      headers,  
      body: JSON.stringify(body),  
      signal: abortSignal,  
      onMessage: (data) => {  
        var _a, _b, _c;  
        if (data === "[DONE]") {  
          return resolve(result);  
        }  
        try {  
          const convoResponseEvent = JSON.parse(data);  
          if (convoResponseEvent.conversation_id) {  
            result.conversationId = convoResponseEvent.conversation_id;  
          }  
          if ((_a = convoResponseEvent.message) == null ? void 0 : _a.id) {  
            result.id = convoResponseEvent.message.id;  
          }  
          const message = convoResponseEvent.message;  
          if (message) {  
            let text2 = (_c = (_b = message == null ? void 0 : message.content) == null ? void 0 : _b.parts) == null ? void 0 : _c[0];  
            if (text2) {  
              result.text = text2;  
              if (onProgress) {  
                onProgress(result);  
              }  
            }  
          }  
        } catch (err) {  
        }  
      }  
    },  
    this._fetch  
  ).catch((err) => {  
    const errMessageL = err.toString().toLowerCase();  
    if (result.text && (errMessageL === "error: typeerror: terminated" || errMessageL === "typeerror: terminated")) {  
      return resolve(result);  
    } else {  
      return reject(err);  
    }  
  });  
});

nginx配置

在搭建过程中,我还遇到另一个坑。因为自己中间有一层nginx代理,而「nginx默认开启了缓存,所以导致流式输出到nginx这个地方被缓存了」,最终前端拿到的数据是缓存后一次性输出的。同时gzip也可能有影响。

这里可以通过nginx配置,把gzip和缓存都关掉。

gzip off;

location / {
		proxy_set_header   Host             $host;
		proxy_set_header   X-Real-IP        $remote_addr;
		proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
		proxy_cache off;
		proxy_cache_bypass $http_pragma;
		proxy_cache_revalidate on;
		proxy_http_version 1.1;
		proxy_buffering off;
		proxy_pass http://xxx.com:1234;
}

proxy_cacheproxy_buffering 是 Nginx 的两个重要的代理模块。它们可以显著提高代理服务器的性能和安全性。

  • proxy_cache:通过这个模块,Nginx 可以缓存代理服务器从后端服务器请求到的响应数据。当下一个客户端请求相同的资源时,Nginx 可以直接从缓存中返回响应,而不必去请求后端服务器。这大大降低了代理服务器的负载,同时也能提高客户端访问速度。需要注意的是,使用 proxy_cache 模块时需要谨慎配置缓存策略,避免出现缓存不一致或者过期的情况。

  • proxy_buffering:通过这个模块,Nginx 可以将后端服务器响应数据缓冲起来,并在完整的响应数据到达之后再将其发送给客户端。这种方式可以减少代理服务器和客户端之间的网络连接数,提高并发处理能力,同时也可以防止后端服务器过早关闭连接,导致客户端无法接收到完整的响应数据。

实测只配置proxy_cache没有用,配置了proxy_buffering后流式输出才生效。

如何实现chatgpt的打字机效果

关于作者

我是Yasin,一个爱写博客的技术人

微信公众号:编了个程(blgcheng)

个人网站:https://yasinshaw.com

欢迎关注这个公众号如何实现chatgpt的打字机效果文章来源地址https://www.toymoban.com/news/detail-417127.html

到了这里,关于如何实现chatgpt的打字机效果的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Vue3实现打字机效果

    typeit 是一款轻量级打字机特效插件。该打印机特效可以设置打字速度,是否显示光标,是否换行和延迟时间等属性,它可以打印单行文本和多行文本,并具有可缩放、响应式等特点。官方文档

    2024年02月02日
    浏览(41)
  • 大模型问答助手前端实现打字机效果

    随着现代技术的快速发展,即时交互变得越来越重要。用户不仅希望获取信息,而且希望以更直观和实时的方式体验它。这在聊天应用程序和其他实时通信工具中尤为明显,用户习惯看到对方正在输入的提示。 ChatGPT,作为 OpenAI 的代表性产品之一,不仅为用户提供了强大的自

    2024年02月08日
    浏览(27)
  • 记录--20行js就能实现逐字显示效果???-打字机效果

    横版 竖版 可以看到文字是一段一段的并且独占一行,使用段落标签p表示一行 一段文字内,字是一个一个显示的,所以这里每一个字都用一个span标签装起来 每一个字都是从透明到不透明的过渡效果,使用css3的过渡属性transition让每个字都从透明过渡到不透明 这里只需要一个

    2024年02月06日
    浏览(28)
  • 前端发送Fetch请求实现流式请求、模拟打字机效果等

    前端需要接收后端的流式返回数据,并实时渲染。 普通的xhr请求都是等http协议数据包一次性返回之后才渲染,类似于ChatGPT的Http接口内容类型为text/event-stream。这种内容类型需要与浏览器建立持久连接并持续监听服务器返回的数据。 npm 方式安装类库 使用 调用 fetchEventSource

    2024年02月13日
    浏览(29)
  • Unity Text文本实现打字机(一个一个出来)的效果

    Unity Text文本要实现打字机,即一个个文字出来的效果,可以通过代码把text文本字符串拆成一个个字符然后添加到文本中。 具体实现: 新建一个控制脚本:TypewriteController.cs,并编写以下代码: 此控制脚本先把脚本文本获取后赋给一个字符串变量,然后置空文本内容,再通过

    2024年01月25日
    浏览(32)
  • Vue3实现酷炫打字机效果:让你的网站文字动起来

    ✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: 三十天精通 Vue 3

    2024年02月05日
    浏览(36)
  • 【JS真好玩】自动打字机效果

    大家好,今天实现一个自动打字机效果,旨在实现一些网上很小的demo样例,通过每一个小demo能够巩固一下我们的前端基础知识。 今天,主要利用定时器、flex布局实现一个自动打字机效果。 效果展示 : 考察 : flex布局、定时器、字符串 建议用时20~35min 我们主要把自动打字

    2024年02月10日
    浏览(29)
  • 仅使用 CSS 创建打字机动画效果

    创建打字机效果比您想象的要容易。虽然实现这种效果的最常见方法是使用 JavaScript,但我们也可以使用纯 CSS 来创建我们的打字机动画。 在本文中,我们将了解如何仅使用 CSS 创建打字机动画效果。它简单、漂亮、容易。我们还将看看使用 CSS 与 JavaScript 创建这种效果的利弊

    2024年02月13日
    浏览(38)
  • 聊聊大模型"打字机"效果的背后技术——SSE

    转载请注明出处:https://www.cnblogs.com/zhiyong-ITNote SSE:Server Sent Event;服务器发送事件。 Server-Sent Events(SSE)是一种由服务器向客户端推送实时数据的技术。它是构建基于事件的、服务器到客户端的通信的一种方法,特别适用于需要实时更新和推送信息的应用场景,如实时通知

    2024年03月27日
    浏览(38)
  • 【CSS3】CSS3 动画 ⑤ ( 动画速度曲线 | 设置动画步长 | 动画匀速执行 | 动画分 2 步执行 | 使用动画步长实现打字机效果 )

    CSS3 样式中 , 设置 动画速度曲线 的属性是 animation-timing-function 属性 ; animation-timing-function 属性定义了动画从 初始 CSS 样式 变为 结束状态 时 所消耗的时间 ; animation-timing-function 属性常用 属性值 如下 : linear : 动画在整个执行过程中速度都是匀速的 ; ease : 默认属性值 , 动画首先

    2024年02月13日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包