openai Realtime API (实时语音)

https://openai.com/index/introducing-the-realtime-api/

 

官方demo

https://github.com/openai/openai-realtime-console

官方demo使用到的插件

https://github.com/openai/openai-realtime-api-beta?tab=readme-ov-file

装包配置

修改yarn.lock 这个包是从github下载的

"@openai/realtime-api-beta@openai/openai-realtime-api-beta":
  version "0.0.0"
  resolved "https://codeload.github.com/openai/openai-realtime-api-beta/tar.gz/a5cb94824f625423858ebacb9f769226ca98945f"
  dependencies:
    ws "^8.18.0"

前端代码

import { RealtimeClient } from '@openai/realtime-api-beta'


 

nginx配置

RealtimeClient需要配置一个wss地址

wss和https使用相同的加密协议,不需要单独配置,直接配置一个转发就可以了

    # https
    server {
        listen       443 ssl; 
        server_name  chat.xutongbao.top;
        # 付费
        ssl_certificate         /temp/ssl/chat.xutongbao.top/chat.xutongbao.top_cert_chain.pem;   # nginx的ssl证书文件
        ssl_certificate_key     /temp/ssl/chat.xutongbao.top/chat.xutongbao.top_key.key;  # nginx的ssl证书验证密码

        # 免费
        # ssl_certificate         /temp/ssl/cersign/chat.xutongbao.top/chat.xutongbao.top.crt;   # nginx的ssl证书文件
        # ssl_certificate_key     /temp/ssl/cersign/chat.xutongbao.top/chat.xutongbao.top_rsa.key;  # nginx的ssl证书验证密码

        proxy_send_timeout 6000s;    # 设置发送超时时间,
        proxy_read_timeout 6000s;    # 设置读取超时时间。

        #配置根目录
        location / {
            root    /temp/yuying;
            index  index.html index.htm;
            add_header Content-Security-Policy upgrade-insecure-requests;

        }

        location /api/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;

            proxy_set_header Connection '';
            proxy_http_version 1.1;
            chunked_transfer_encoding off;
            proxy_buffering off;
            proxy_cache off;

            proxy_pass http://yuying-api.xutongbao.top;
        }

        location /socket.io/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
            proxy_pass http://127.0.0.1:84;

            # 关键配置 start
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            # 关键配置 end
        }

        location /ws {
            proxy_pass http://52.247.xxx.xxx:86/;
            proxy_read_timeout              500;
            proxy_set_header                Host    $http_host;
            proxy_set_header                X-Real-IP          $remote_addr;
            proxy_set_header                X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_http_version 1.1;
            
            # ws 协议专用头
            proxy_set_header                Upgrade $http_upgrade;
            proxy_set_header                Connection "Upgrade";
        }

        location /ws-test {
            proxy_pass http://52.247.xxx.xxx:92/;
            proxy_read_timeout              500;
            proxy_set_header                Host    $http_host;
            proxy_set_header                X-Real-IP          $remote_addr;
            proxy_set_header                X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_http_version 1.1;
            
            # ws 协议专用头
            proxy_set_header                Upgrade $http_upgrade;
            proxy_set_header                Connection "Upgrade";
        }


        # 匹配sslCnd开头的请求,实际转发的请求去掉多余的sslCnd这三个字母
        location ^~/sslCnd/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-NginX-Proxy true;
            proxy_pass http://cdn.xutongbao.top/;
        }           
    }   

建立连接时如何通过token确认用户身份

  let apiKeyValue = `${localStorage.getItem(
    'token'
  )}divide${localStorage.getItem('talkId')}`
  const clientRef = useRef(
    new RealtimeClient(
      LOCAL_RELAY_SERVER_URL
        ? {
            url: LOCAL_RELAY_SERVER_URL,
            apiKey: apiKeyValue,
            dangerouslyAllowAPIKeyInBrowser: true,
          }
        : {
            apiKey: apiKey,
            dangerouslyAllowAPIKeyInBrowser: true,
          }
    )
  )

前端完整代码

realtimePlus/pages/ConsolePage.js:

import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { useEffect, useRef, useCallback, useState } from 'react'
import { RealtimeClient } from '@openai/realtime-api-beta'
import { WavRecorder, WavStreamPlayer } from '../lib/wavtools/index.js'
import { instructions } from '../utils/conversation_config.js'
import { WavRenderer } from '../utils/wav_renderer'
import { X, ArrowUp, ArrowDown } from 'react-feather'
import { Button, Dropdown, Input, Select } from 'antd'
import { SinglePageHeader, Icon } from '../../../../../../components/light'
import { isPC } from '../../../../../../utils/tools.js'
import { realTimeBaseURL } from '../../../../../../utils/config.js'
import { message as antdMessage } from 'antd'
import Api from '../../../../../../api/index.js'

import './ConsolePage.css'
import './index.css'
const LOCAL_RELAY_SERVER_URL = realTimeBaseURL //'wss://chat.xutongbao.top/ws'

const Option = Select.Option
let isPCFlag = isPC()
let isAddStart = false
let addIdHistory = []

function Index() {
  //#region 配置
  const apiKey = LOCAL_RELAY_SERVER_URL
    ? ''
    : localStorage.getItem('tmp::voice_api_key') ||
      prompt('OpenAI API Key') ||
      ''
  if (apiKey !== '') {
    localStorage.setItem('tmp::voice_api_key', apiKey)
  }

  const wavRecorderRef = useRef(new WavRecorder({ sampleRate: 24000 }))
  const wavStreamPlayerRef = useRef(new WavStreamPlayer({ sampleRate: 24000 }))
  let apiKeyValue = `${localStorage.getItem(
    'token'
  )}divide${localStorage.getItem('talkId')}`
  const clientRef = useRef(
    new RealtimeClient(
      LOCAL_RELAY_SERVER_URL
        ? {
            url: LOCAL_RELAY_SERVER_URL,
            apiKey: apiKeyValue,
            dangerouslyAllowAPIKeyInBrowser: true,
          }
        : {
            apiKey: apiKey,
            dangerouslyAllowAPIKeyInBrowser: true,
          }
    )
  )

  const clientCanvasRef = useRef(null)
  const serverCanvasRef = useRef(null)
  const eventsScrollHeightRef = useRef(0)
  const eventsScrollRef = useRef(null)
  const startTimeRef = useRef(new Date().toISOString())

  const [items, setItems] = useState([])
  const [realtimeEvents, setRealtimeEvents] = useState([])
  const [expandedEvents, setExpandedEvents] = useState({})
  const [isConnected, setIsConnected] = useState(false)
  const [canPushToTalk, setCanPushToTalk] = useState(true)
  const [isRecording, setIsRecording] = useState(false)
  const [message, setMessage] = useState('')
  const [messageType, setMessageType] = useState('none')
  //#endregion

  const getItems = () => {
    const items = [
      {
        key: 'chrome',
        label: (
          <>
            {/* eslint-disable-next-line */}
            <a
              href={`https://static.xutongbao.top/app/ChromeSetup.exe`}
              target="_blank"
            >
              下载chrome浏览器(推荐)
            </a>
          </>
        ),
        icon: <Icon name="chrome" className="m-realtime-menu-icon"></Icon>,
      },
    ]
    return items
  }

  //#region 基础
  const formatTime = useCallback((timestamp) => {
    const startTime = startTimeRef.current
    const t0 = new Date(startTime).valueOf()
    const t1 = new Date(timestamp).valueOf()
    const delta = t1 - t0
    const hs = Math.floor(delta / 10) % 100
    const s = Math.floor(delta / 1000) % 60
    const m = Math.floor(delta / 60_000) % 60
    const pad = (n) => {
      let s = n + ''
      while (s.length < 2) {
        s = '0' + s
      }
      return s
    }
    return `${pad(m)}:${pad(s)}.${pad(hs)}`
  }, [])

  const connectConversation = useCallback(async () => {
    const client = clientRef.current
    const wavRecorder = wavRecorderRef.current
    const wavStreamPlayer = wavStreamPlayerRef.current

    startTimeRef.current = new Date().toISOString()
    setIsConnected(true)
    setRealtimeEvents([])
    setItems(client.conversation.getItems())

    try {
      // Connect to microphone
      await wavRecorder.begin()
    } catch (error) {
      console.log(error)
    }

    // Connect to audio output
    await wavStreamPlayer.connect()

    // Connect to realtime API
    await client.connect()
    // let isAutoAsk = true
    // if (isAutoAsk) {
    // client.sendUserMessageContent([
    //   {
    //     type: `input_text`,
    //     text: `你好!`,
    //   },
    // ])

    if (client.getTurnDetectionType() === 'server_vad') {
      await wavRecorder.record((data) => client.appendInputAudio(data.mono))
    }
  }, [])

  const handleTest = () => {
    const client = clientRef.current
    client.sendUserMessageContent([
      {
        type: `input_text`,
        text: message,
      },
    ])
    setMessage('')
  }

  const handleMessage = (event) => {
    setMessage(event.target.value)
  }

  /**
   * Disconnect and reset conversation state
   */
  const disconnectConversation = useCallback(async () => {
    setIsConnected(false)
    setRealtimeEvents([])
    // setItems([])

    const client = clientRef.current
    client.disconnect()

    const wavRecorder = wavRecorderRef.current
    await wavRecorder.end()

    const wavStreamPlayer = wavStreamPlayerRef.current
    await wavStreamPlayer.interrupt()
  }, [])

  const deleteConversationItem = useCallback(async (id) => {
    const client = clientRef.current
    client.deleteItem(id)
  }, [])

  /**
   * In push-to-talk mode, start recording
   * .appendInputAudio() for each sample
   */
  const startRecording = async () => {
    setIsRecording(true)
    const client = clientRef.current
    const wavRecorder = wavRecorderRef.current
    const wavStreamPlayer = wavStreamPlayerRef.current
    const trackSampleOffset = await wavStreamPlayer.interrupt()
    if (trackSampleOffset?.trackId) {
      const { trackId, offset } = trackSampleOffset
      await client.cancelResponse(trackId, offset)
    }
    try {
      await wavRecorder.record((data) => client.appendInputAudio(data.mono))
    } catch (error) {
      console.log(error)
    }
  }

  /**
   * In push-to-talk mode, stop recording
   */
  const stopRecording = async () => {
    setIsRecording(false)
    const client = clientRef.current
    const wavRecorder = wavRecorderRef.current
    try {
      await wavRecorder.pause()
    } catch (error) {
      console.log(error)
    }
    try {
      client.createResponse()
    } catch (error) {
      console.log(error)
    }
  }

  /**
   * Switch between Manual <> VAD mode for communication
   */
  const changeTurnEndType = async (messageType) => {
    setMessageType(messageType)
    let value
    if (messageType === 'server_vad') {
      value = 'server_vad'
    } else if (messageType === 'none' || messageType === 'input') {
      value = 'none'
    }
    const client = clientRef.current
    const wavRecorder = wavRecorderRef.current
    if (value === 'none' && wavRecorder.getStatus() === 'recording') {
      await wavRecorder.pause()
    }
    client.updateSession({
      turn_detection: value === 'none' ? null : { type: 'server_vad' },
    })
    if (value === 'server_vad' && client.isConnected()) {
      await wavRecorder.record((data) => client.appendInputAudio(data.mono))
    }
    setCanPushToTalk(messageType === 'none')
  }

  const handleSearch = () => {
    let params = {
      talkId: localStorage.getItem('talkId'),
      gptVersion: 'realtime',
      pageNum: 1,
      pageSize: 20,
      isGetNewest: true,
    }

    const client = clientRef.current
    // client.conversation.processEvent({
    //   type: 'conversation.item.created',
    //   event_id: 'item_ARaEpHPCznsNlBGN5DGFp',
    //   item: {
    //     id: 'item_ARaEpHPCznsNlBGN5DGFp',
    //     object: 'realtime.item',
    //     type: 'message',
    //     status: 'completed',
    //     role: 'user',
    //     content: [{ type: 'input_text', text: '你好' }],
    //     formatted: { audio: {}, text: '你好', transcript: '' },
    //   }
    // })
    // let items = client.conversation.getItems()
    // console.log('items', items)

    Api.h5.chatSearch(params).then((res) => {
      // let list = [
      //   {
      //     id: 'item_ARaEpHPCznsNlBGN5DGFp',
      //     object: 'realtime.item',
      //     type: 'message',
      //     status: 'completed',
      //     role: 'user',
      //     content: [{ type: 'input_text', text: '你好' }],
      //     formatted: { audio: {}, text: '你好', transcript: '' },
      //   },
      //   {
      //     id: 'item_ARaEpLuspCKg6raB95pFr',
      //     object: 'realtime.item',
      //     type: 'message',
      //     status: 'in_progress',
      //     role: 'assistant',
      //     content: [{ type: 'audio', transcript: '你好!' }],
      //     formatted: { audio: {}, text: '', transcript: '你好!' },
      //   },
      // ]

      if (res.code === 200) {
        let list = res.data.list.map((item) => {
          return {
            id: item.uid,
            object: 'realtime.item',
            type: 'message',
            status: 'completed',
            role: item.messageType === '1' ? 'user' : 'assistant',
            content: [
              {
                type: item.messageType === '1' ? 'input_text' : 'text',
                text: item.message,
                transcript: item.message,
              },
            ],
            formatted: {
              audio: {},
              text: item.message,
              transcript: item.message,
            },
          }
        })
        setItems(list)
        list.forEach((item) => {
          client.conversation.processEvent({
            type: 'conversation.item.created',
            event_id: item.id,
            item: {
              ...item,
            },
          })
        })
        let items = client.conversation.getItems()
        console.log('items', items)
      }
    })
  }

  //#endregion

  //#region  useEffect
  /**
   * Auto-scroll the event logs
   */
  useEffect(() => {
    if (eventsScrollRef.current) {
      const eventsEl = eventsScrollRef.current
      const scrollHeight = eventsEl.scrollHeight
      // Only scroll if height has just changed
      if (scrollHeight !== eventsScrollHeightRef.current) {
        eventsEl.scrollTop = scrollHeight
        eventsScrollHeightRef.current = scrollHeight
      }
    }
  }, [realtimeEvents])

  /**
   * Auto-scroll the conversation logs
   */
  useEffect(() => {
    const conversationEls = [].slice.call(
      document.body.querySelectorAll('[data-conversation-content]')
    )
    for (const el of conversationEls) {
      const conversationEl = el
      conversationEl.scrollTop = conversationEl.scrollHeight
    }
  }, [items])

  /**
   * Set up render loops for the visualization canvas
   */
  useEffect(() => {
    let isLoaded = true

    const wavRecorder = wavRecorderRef.current
    const clientCanvas = clientCanvasRef.current
    let clientCtx = null

    const wavStreamPlayer = wavStreamPlayerRef.current
    const serverCanvas = serverCanvasRef.current
    let serverCtx = null

    const render = () => {
      if (isLoaded) {
        if (clientCanvas) {
          if (!clientCanvas.width || !clientCanvas.height) {
            clientCanvas.width = clientCanvas.offsetWidth
            clientCanvas.height = clientCanvas.offsetHeight
          }
          clientCtx = clientCtx || clientCanvas.getContext('2d')
          if (clientCtx) {
            clientCtx.clearRect(0, 0, clientCanvas.width, clientCanvas.height)
            const result = wavRecorder.recording
              ? wavRecorder.getFrequencies('voice')
              : { values: new Float32Array([0]) }
            WavRenderer.drawBars(
              clientCanvas,
              clientCtx,
              result.values,
              '#0099ff',
              10,
              0,
              8
            )
          }
        }
        if (serverCanvas) {
          if (!serverCanvas.width || !serverCanvas.height) {
            serverCanvas.width = serverCanvas.offsetWidth
            serverCanvas.height = serverCanvas.offsetHeight
          }
          serverCtx = serverCtx || serverCanvas.getContext('2d')
          if (serverCtx) {
            serverCtx.clearRect(0, 0, serverCanvas.width, serverCanvas.height)
            const result = wavStreamPlayer.analyser
              ? wavStreamPlayer.getFrequencies('voice')
              : { values: new Float32Array([0]) }
            WavRenderer.drawBars(
              serverCanvas,
              serverCtx,
              result.values,
              '#009900',
              10,
              0,
              8
            )
          }
        }
        window.requestAnimationFrame(render)
      }
    }
    render()

    return () => {
      isLoaded = false
    }
  }, [])

  /**
   * Core RealtimeClient and audio capture setup
   * Set all of our instructions, tools, events and more
   */
  useEffect(() => {
    // Get refs
    const wavStreamPlayer = wavStreamPlayerRef.current
    const client = clientRef.current

    // Set instructions
    client.updateSession({ instructions: instructions })
    // Set transcription, otherwise we don't get user transcriptions back
    client.updateSession({ input_audio_transcription: { model: 'whisper-1' } })

    // handle realtime events from client + server for event logging
    client.on('realtime.event', (realtimeEvent) => {
      if (realtimeEvent.event.code === 400) {
        antdMessage.warning(realtimeEvent.event.message)
        disconnectConversation()
        return
      }
      setRealtimeEvents((realtimeEvents) => {
        const lastEvent = realtimeEvents[realtimeEvents.length - 1]
        if (lastEvent?.event.type === realtimeEvent.event.type) {
          // if we receive multiple events in a row, aggregate them for display purposes
          lastEvent.count = (lastEvent.count || 0) + 1
          return realtimeEvents.slice(0, -1).concat(lastEvent)
        } else {
          return realtimeEvents.concat(realtimeEvent)
        }
      })
    })
    client.on('error', (event) => console.error(event))
    client.on('conversation.interrupted', async () => {
      const trackSampleOffset = await wavStreamPlayer.interrupt()
      if (trackSampleOffset?.trackId) {
        const { trackId, offset } = trackSampleOffset
        await client.cancelResponse(trackId, offset)
      }
    })
    client.on('conversation.updated', async ({ item, delta }) => {
      const items = client.conversation.getItems()
      if (delta?.audio) {
        wavStreamPlayer.add16BitPCM(delta.audio, item.id)
      }
      if (item.status === 'completed' && item.formatted.audio?.length) {
        const wavFile = await WavRecorder.decode(
          item.formatted.audio,
          24000,
          24000
        )
        item.formatted.file = wavFile
      }
      setItems(items)
      isAddStart = true
    })

    setItems(client.conversation.getItems())
    handleSearch()

    return () => {
      // cleanup; resets to defaults
      client.reset()
    }
    // eslint-disable-next-line
  }, [])

  useEffect(() => {
    if (Array.isArray(items) && items.length > 0) {
      let lastItem = items[items.length - 1]

      let addIdHistoryIndex = addIdHistory.findIndex(
        (item) => item === lastItem.id
      )
      if (
        lastItem?.status === 'completed' &&
        lastItem?.role === 'assistant' &&
        isAddStart === true &&
        addIdHistoryIndex < 0
      ) {
        addIdHistory.push(lastItem.id)
        let message = items[items.length - 2].formatted.transcript
          ? items[items.length - 2].formatted.transcript
          : items[items.length - 2].formatted.text
        let robotMessage = lastItem.formatted.transcript
        Api.h5
          .chatRealTimeAdd({
            talkId: localStorage.getItem('talkId'),
            name: localStorage.getItem('nickname'),
            message,
            robotMessage,
          })
          .then((res) => {
            if (res.code === 40006) {
              antdMessage.warning(res.message)
              disconnectConversation()
            }
          })
      }
    }

    // eslint-disable-next-line
  }, [items, isAddStart])
  //#endregion

  return (
    <div className="m-realtime-wrap-box">
      <div className={`m-realtime-wrap-chat`}>
        <SinglePageHeader
          goBackPath="/ai/index/home/chatList"
          title="Realtime"
        ></SinglePageHeader>
        <div className="m-realtime-list" id="scrollableDiv">
          {window.platform === 'rn' ? null : (
            <Dropdown
              menu={{ items: getItems() }}
              className="m-realtime-dropdown"
              trigger={['click', 'hover']}
            >
              <Icon name="more" className="m-realtime-menu-btn"></Icon>
            </Dropdown>
          )}
          <div data-component="ConsolePage">
            <div className="content-main">
              <div className="content-logs">
                <div className="content-block events">
                  <div className="visualization">
                    <div className="visualization-entry client">
                      <canvas ref={clientCanvasRef} />
                    </div>
                    <div className="visualization-entry server">
                      <canvas ref={serverCanvasRef} />
                    </div>
                  </div>
                  <div className="content-block-body" ref={eventsScrollRef}>
                    {!realtimeEvents.length && `等待连接...`}
                    {realtimeEvents.map((realtimeEvent, i) => {
                      const count = realtimeEvent.count
                      const event = { ...realtimeEvent.event }
                      if (event.type === 'input_audio_buffer.append') {
                        event.audio = `[trimmed: ${event.audio.length} bytes]`
                      } else if (event.type === 'response.audio.delta') {
                        event.delta = `[trimmed: ${event.delta.length} bytes]`
                      }
                      return (
                        <div className="event" key={event.event_id}>
                          <div className="event-timestamp">
                            {formatTime(realtimeEvent.time)}
                          </div>
                          <div className="event-details">
                            <div
                              className="event-summary"
                              onClick={() => {
                                // toggle event details
                                const id = event.event_id
                                const expanded = { ...expandedEvents }
                                if (expanded[id]) {
                                  delete expanded[id]
                                } else {
                                  expanded[id] = true
                                }
                                setExpandedEvents(expanded)
                              }}
                            >
                              <div
                                className={`event-source ${
                                  event.type === 'error'
                                    ? 'error'
                                    : realtimeEvent.source
                                }`}
                              >
                                {realtimeEvent.source === 'client' ? (
                                  <ArrowUp />
                                ) : (
                                  <ArrowDown />
                                )}
                                <span>
                                  {event.type === 'error'
                                    ? 'error!'
                                    : realtimeEvent.source}
                                </span>
                              </div>
                              <div className="event-type">
                                {event.type}
                                {count && ` (${count})`}
                              </div>
                            </div>
                            {!!expandedEvents[event.event_id] && (
                              <div className="event-payload">
                                {JSON.stringify(event, null, 2)}
                              </div>
                            )}
                          </div>
                        </div>
                      )
                    })}
                  </div>
                </div>
                <div className="content-block conversation">
                  <div className="content-block-body" data-conversation-content>
                    {!items.length && `等待连接...`}
                    {items.map((conversationItem, i) => {
                      return (
                        <div
                          className="conversation-item"
                          key={conversationItem.id}
                        >
                          <div
                            className={`speaker ${conversationItem.role || ''}`}
                          >
                            <div>
                              {(
                                conversationItem.role || conversationItem.type
                              ).replaceAll('_', ' ')}
                            </div>
                            <div
                              className="close"
                              onClick={() =>
                                deleteConversationItem(conversationItem.id)
                              }
                            >
                              <X />
                            </div>
                          </div>
                          <div className={`speaker-content`}>
                            {/* tool response */}
                            {conversationItem.type ===
                              'function_call_output' && (
                              <div>{conversationItem.formatted.output}</div>
                            )}
                            {/* tool call */}
                            {!!conversationItem.formatted.tool && (
                              <div>
                                {conversationItem.formatted.tool.name}(
                                {conversationItem.formatted.tool.arguments})
                              </div>
                            )}
                            {!conversationItem.formatted.tool &&
                              conversationItem.role === 'user' && (
                                <div className="m-realtime-message">
                                  {conversationItem.formatted.transcript ||
                                    (conversationItem.formatted.audio?.length
                                      ? '(awaiting transcript)'
                                      : conversationItem.formatted.text ||
                                        '(item sent)')}
                                </div>
                              )}
                            {!conversationItem.formatted.tool &&
                              conversationItem.role === 'assistant' && (
                                <div className="m-realtime-message">
                                  {conversationItem.formatted.transcript ||
                                    conversationItem.formatted.text ||
                                    '(truncated)'}
                                </div>
                              )}
                            {conversationItem.formatted.file && (
                              <audio
                                src={conversationItem.formatted.file.url}
                                controls
                              />
                            )}
                          </div>
                        </div>
                      )
                    })}
                  </div>
                </div>
                <div className="content-actions">
                  <Select
                    value={messageType}
                    onChange={(value) => changeTurnEndType(value)}
                    placeholder="请选择"
                  >
                    <Option value="none">手动</Option>
                    <Option value="server_vad">自动</Option>
                    <Option value="input">打字</Option>
                  </Select>
                  <div className="spacer" />
                  {isConnected && canPushToTalk && (
                    <>
                      {isPCFlag ? (
                        <Button
                          type="primary"
                          label={
                            isRecording ? 'release to send' : 'push to talk'
                          }
                          disabled={!isConnected || !canPushToTalk}
                          onMouseDown={startRecording}
                          onMouseUp={stopRecording}
                          className={`m-realtime-recorad-btn ${
                            isRecording ? 'active' : ''
                          }`}
                        >
                          {isRecording ? '松开发送' : '按住说话'}
                        </Button>
                      ) : (
                        <Button
                          type="primary"
                          label={
                            isRecording ? 'release to send' : 'push to talk'
                          }
                          disabled={!isConnected || !canPushToTalk}
                          onTouchStart={startRecording}
                          onTouchEnd={stopRecording}
                          className={`m-realtime-recorad-btn ${
                            isRecording ? 'active' : ''
                          }`}
                        >
                          {isRecording ? '松开发送' : '按住说话'}
                        </Button>
                      )}
                    </>
                  )}
                  {isConnected && messageType === 'input' ? (
                    <div className="m-realtime-input-wrap">
                      <Input.TextArea
                        value={message}
                        onChange={(event) => handleMessage(event)}
                        placeholder="请输入"
                      ></Input.TextArea>
                      <Button
                        type="primary"
                        onClick={() => handleTest()}
                        className="m-realtime-send-btn"
                      >
                        发送
                      </Button>
                    </div>
                  ) : null}
                  <div className="spacer" />
                  <Button
                    type="primary"
                    danger={isConnected ? true : false}
                    onClick={
                      isConnected ? disconnectConversation : connectConversation
                    }
                  >
                    {isConnected ? '已连接' : '连接'}
                  </Button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

const mapStateToProps = (state) => {
  return {
    collapsed: state.getIn(['light', 'collapsed']),
    isRNGotToken: state.getIn(['light', 'isRNGotToken']),
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    onSetState(key, value) {
      dispatch({ type: 'SET_LIGHT_STATE', key, value })
    },
    onDispatch(action) {
      dispatch(action)
    },
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Index))

后端通过请求头获取token

  async handleUserAuth(req) {
    let index = req.rawHeaders.findIndex((item) =>
      item.includes('realtime, openai-insecure-api-key.')
    )
    let infoValue = ''
    if (index >= 0) {
      infoValue = req.rawHeaders[index]
    }
    infoValue = infoValue.replace('realtime, openai-insecure-api-key.', '')
    infoValue = infoValue.replace(', openai-beta.realtime-v1', '')
    let infoValueArr = infoValue.split('divide')
    let realTimeAuthRes = await axios.post(
      `${baseURL}/api/light/chat/realTimeAuth`,
      {
        token: infoValueArr[0],
        talkId: infoValueArr[1],
        apiKey,
      }
    )
    return realTimeAuthRes
  }

后端完整代码

relay.js:

const { WebSocketServer } = require('ws')
const axios = require('axios')

let baseURL = process.env.aliIPAddressWithPort
let apiKey = process.env.apiKeyOnServer

class RealtimeRelay {
  constructor(apiKey) {
    this.apiKey = apiKey
    this.sockets = new WeakMap()
    this.wss = null
  }

  listen(port) {
    this.wss = new WebSocketServer({ port })
    this.wss.on('connection', this.connectionHandler.bind(this))
    this.log(`Listening on ws://localhost:${port}`)
  }

  async handleUserAuth(req) {
    let index = req.rawHeaders.findIndex((item) =>
      item.includes('realtime, openai-insecure-api-key.')
    )
    let infoValue = ''
    if (index >= 0) {
      infoValue = req.rawHeaders[index]
    }
    infoValue = infoValue.replace('realtime, openai-insecure-api-key.', '')
    infoValue = infoValue.replace(', openai-beta.realtime-v1', '')
    let infoValueArr = infoValue.split('divide')
    let realTimeAuthRes = await axios.post(
      `${baseURL}/api/light/chat/realTimeAuth`,
      {
        token: infoValueArr[0],
        talkId: infoValueArr[1],
        apiKey,
      }
    )
    return realTimeAuthRes
  }

  async connectionHandler(ws, req) {
    if (global.isAzure) {
      let realTimeAuthRes = await this.handleUserAuth(req)
      if (realTimeAuthRes.data.code === 200) {
        let Realtime = await import('@openai/realtime-api-beta')
        const { RealtimeClient } = Realtime
        if (!req.url) {
          this.log('No URL provided, closing connection.')
          ws.close()
          return
        }

        const url = new URL(req.url, `http://${req.headers.host}`)
        const pathname = url.pathname

        if (pathname !== '/') {
          this.log(`Invalid pathname: "${pathname}"`)
          ws.close()
          return
        }

        // Instantiate new client
        this.log(`Connecting with key "${this.apiKey.slice(0, 3)}..."`)
        const client = new RealtimeClient({ apiKey: this.apiKey })

        // Relay: OpenAI Realtime API Event -> Browser Event
        client.realtime.on('server.*', (event) => {
          this.log(`Relaying "${event.type}" to Client`)
          ws.send(JSON.stringify(event))
        })
        client.realtime.on('close', () => ws.close())

        // Relay: Browser Event -> OpenAI Realtime API Event
        // We need to queue data waiting for the OpenAI connection
        const messageQueue = []
        const messageHandler = (data) => {
          try {
            const event = JSON.parse(data)
            this.log(`Relaying "${event.type}" to OpenAI`)
            client.realtime.send(event.type, event)
          } catch (e) {
            console.error(e.message)
            this.log(`Error parsing event from client: ${data}`)
          }
        }
        ws.on('message', (data) => {
          if (!client.isConnected()) {
            messageQueue.push(data)
          } else {
            messageHandler(data)
          }
        })
        ws.on('close', () => client.disconnect())

        // Connect to OpenAI Realtime API
        try {
          this.log(`Connecting to OpenAI...`)
          await client.connect()
        } catch (e) {
          this.log(`Error connecting to OpenAI: ${e.message}`)
          ws.close()
          return
        }
        this.log(`Connected to OpenAI successfully!`)
        while (messageQueue.length) {
          messageHandler(messageQueue.shift())
        }
      } else {
        ws.send(
          JSON.stringify({
            ...realTimeAuthRes.data,
          })
        )
      }
    }
  }

  // eslint-disable-next-line
  log(...args) {
    // console.log(`[RealtimeRelay]`, ...args)
  }
}

module.exports = {
  RealtimeRelay,
}

调用上面的代码:

  const relay = new RealtimeRelay(process.env.openaiToken)
  relay.listen(PORT)

人工智能学习网站

https://chat.xutongbao.top

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/914230.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Docker--Docker是什么和对Docker的了解

Docker 的本质 Docker的本质是LXC&#xff08;Linux容器&#xff09;之类的增强版&#xff0c;它本身不是容器&#xff0c;而是容器的易用工具。 Docker通过虚拟化技术&#xff0c;将代码、依赖项和运行环境打包成一个容器&#xff0c;并利用隔离机制来使得容器之间互相独立、…

【报错记录】Steam迁移(移动)游戏报:移动以下应用的内容失败:XXX: 磁盘写入错误

前言 由于黑神话悟空&#xff0c;导致我的2TB的SSD系统盘快满了&#xff0c;我又买了一块4TB的SSD用来存放游戏&#xff0c;我就打算把之前C盘里的游戏移动到D盘&#xff0c;结果Steam移动游戏居然报错了&#xff0c;报的还是“磁盘写入错误”&#xff0c;如下图所示&#xff…

攻防世界37-unseping-CTFWeb

攻防世界37-unseping-CTFWeb <?php highlight_file(__FILE__);class ease{private $method;private $args;function __construct($method, $args) {$this->method $method;$this->args $args;}function __destruct(){if (in_array($this->method, array("…

LabVIEW 版本控制

在软件开发中&#xff0c;版本控制系统&#xff08;VCS&#xff09; 是管理代码版本变化的核心工具。对于 LabVIEW 用户&#xff0c;虽然图形化编程带来高效开发体验&#xff0c;但由于其特有的二进制 VI 文件格式&#xff0c;传统文本比较工具无法直接用于 LabVIEW 项目。这时…

Cesium着色器的创意和方法(五——Polyline)

不接受反驳&#xff0c;线段在三维里面的渲染比多边形的渲染更复杂。因为线是没有宽度的&#xff0c;并且还需要时时刻刻向着你。 首先看下Cesium的线段的一些效果。这条线段非常宽&#xff08;20个像素&#xff09;&#xff0c;有两个需要留意观察的。一是线段并非实时面对我…

独立开发者赚钱心法

一、独立开发者的身份转变 开发者到多重角色&#xff1a;独立开发者不仅是程序员&#xff0c;还是产品经理、设计师、文案、销售、运营、客服&#xff0c;最重要的是成为“老板”。思维转变&#xff1a;将开发程序视为一门生意&#xff0c;而非单纯的技术实现。 二、赚钱的核…

Hook小程序

下载&#xff1a; https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python 配置&#xff1a; pip install -r requirements 实现&#xff1a; 开启小程序开发者模式&#xff0c;类似浏览器F12 效果&#xff1a; 使用&#xff1a; 退出微信&#xff0c;进入安装的目录…

【Spring Security系列】10分钟实现 SpringSecurity + CAS 完美单点登录方案

作者&#xff1a;后端小肥肠 &#x1f347; 我写过的文章中的相关代码放到了gitee&#xff0c;地址&#xff1a;xfc-fdw-cloud: 公共解决方案 &#x1f34a; 有疑问可私信或评论区联系我。 &#x1f951; 创作不易未经允许严禁转载。 姊妹篇&#xff1a; 【Spring Security系列…

Spring Boot编程训练系统:构建可扩展的应用

摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了编程训练系统的开发全过程。通过分析编程训练系统管理的不足&#xff0c;创建了一个计算机管理编程训练系统的方案。文章介绍了编程训练系统的系统分析部分&…

提升AI性能的关键大型语言模型(LLM)压缩策略

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

「IDE」集成开发环境专栏目录大纲

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「IDE」集成开发环境&#x1f4da;全部专栏「Win」Windows程序设计「IDE」集成开发环境「UG/NX」BlockUI集合「C/C」C/C程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「UG/NX」NX定…

关于 npm 更新镜像源问题

npm&#xff08;Node Package Manager&#xff09;&#xff0c;是一个NodeJS包管理和分发工具&#xff0c;已经成为了非官方的发布Node模块&#xff08;包&#xff09;的标准。&#xff09; 查看当前npm版本 npm -v 10.9.0 执行以下命令报错 npm install --registryhttp…

Worldly平台更新Higg FEM 2024模块价格及购买指南

近日&#xff0c;LEVERAGE供应链管理从美国可持续服装联盟&#xff08;Cascale&#xff09;验证官方Worldly平台模块订阅更新中获悉&#xff0c;FEM2024模块价格更新的重要信息。此次更新涉及工厂环境模块&#xff08;FEM&#xff09;和工厂社会劳工模块&#xff08;FSLM&#…

MQ的实际使用

前言: 在这一篇文章当中我会以RcoketMQ来对其的使用的场景进行一个仔细地说明,这个里面也会涉及到一些额外的知识,看完之后对面试而言的话那么就是直接拿捏,当然在看这篇文章之前要先看MQ的基础知识-CSDN博客 毕竟基础才是王道,下面就是开始我们的正菜 在我的基础的那篇文章中就…

MFC图形函数学习08——绘图函数的重载介绍

在《MFC图形函数学习06——画椭圆弧线函数》中介绍了CPoint类、POINT结构体&#xff1b;在《MFC图形函数学习07——画扇形函数》中介绍了CRect类、RECT结构体。在介绍完后&#xff0c;没有介绍它们怎样使用。实际上&#xff0c;这些类和结构体对象或指针也是我们学习过的绘图函…

SAP-ABAP开发-BAPI

BAPI基于数据库表的操作函数传入传出数据&#xff0c;本身函数有接口与增强无关 目录 一、BAPI接口定义 二、业务对象 三、查询方法 四、调用 五、BAPI创建 &#xff08;1&#xff09;在DDIC中创建一个结构 &#xff08;2&#xff09;创建BAPI函数模块和函数或API方法 …

Ceph MDS高可用架构探索:从零到一构建多主一备MDS服务

文章目录 Ceph实现MDS服务多主一备高可用架构当前 mds 服务器状态添加 MDS 服务器验证ceph集群当前状态当前的文件系统状态设置处于激活状态 mds 的数量MDS 高可用优化分发配置文件并重启 mds 服务 Ceph实现MDS服务多主一备高可用架构 Ceph 的元数据服务&#xff08;MDS&#…

python实战(八)——情感识别(多分类)

一、任务目标 本文使用的是来自Kaggle的一个情感识别数据集&#xff0c;这个数据集的总数据量是5934条&#xff0c;标签为anger、fear、joy三种情感的其中一种&#xff0c;很明显是一个多分类任务。这里&#xff0c;我们将使用微调技巧进行深度学习建模&#xff0c;同时我们会比…

价格战背后:即时零售三小龙的致命伤

价格战&#xff0c;从来就不仅仅是低价&#xff0c;低价前面永远要加上定语&#xff1a;确保品质和服务的。价格战是减法&#xff0c;更是加法。减去的是价格水分&#xff0c;加上的是品质和服务保障。 转载|原创新熵 作者丨宜新 编辑丨赛柯 今年双十一的热点&#xff0c;让人…

sd1.5/sdxl的推理,训练

1.sd1.5/sdxl的推理 主要讲述一下unet的降噪&#xff0c;以及采样器的作用&#xff0c;已sd1.5为例&#xff0c;sdxl类似 unet的降噪过程中&#xff0c;如20步降噪&#xff0c;这20个unet共用的一个权重 1.1 timesteps 根据unet的降噪步数&#xff0c;即num_inference_steps…