Teams集成-会议侧边栏应用开发-实时转写

news2024/9/27 5:21:42

Teams虽然提供了转写的接口,但是不是实时的,即便使用订阅事件也不是实时的,为了达到实时转写的效果,使用recall.ai的转录和assembly_ai的转写实现。

前提:除Teams会议侧边栏应用开发-会议转写-CSDN博客的基本要求外,还需要修改用户的安全设置及设置Teams 工作账号,参考:Setup Guide (recall.ai)

一、服务端需要实现4个服务端点:

1)开始录音(创建机器人)

/*
 * Send's a Recall Bot to start recording the call
 */
server.post('/start-recording', async (req, res) => {
  const meeting_url = req.body.meetingUrl;
  try {
      if (!meeting_url) {
          return res.status(400).json({ error: 'Missing meetingUrl' });
      }

      console.log('recall bot start recording', meeting_url);

      const url = 'https://us-west-2.recall.ai/api/v1/bot/';
      const options = {
        method: 'POST',
        headers: {
          accept: 'application/json',
          'content-type': 'application/json',
          Authorization: `Token ${RECALL_API_KEY}`
        },
        body: JSON.stringify({
          bot_name: 'teams bot',
          real_time_transcription: {
            destination_url: 'https://shortly-adapted-akita.ngrok-free.app/transcription?secret=' + WEBHOOK_SECRET,
            partial_results: false
          },
          transcription_options: {provider: 'assembly_ai'},
          meeting_url: meeting_url
        })
      };

      const response = await fetch(url, options);
      const bot = await response.json();
      local_botId = bot.id
      console.log('botId:', local_botId);

      res.send(200, JSON.stringify({
          botId: local_botId
      }));
  } catch (error) {
    console.error("start-recoding error:", error);
  }
});

2)停止录音

/*
* Tells the Recall Bot to stop recording the call
*/
server.post('/stop-recording', async (req, res) => {
  try {
      const botId = local_botId;
      if (!botId) {
          res.send(400, JSON.stringify({ error: 'Missing botId' }));
      }
      await fetch(`https://us-west-2.recall.ai/api/v1/bot/${botId}/leave_call`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Token ${RECALL_API_KEY}`
          },
      });
      console.log('recall bot stopped');
      res.send(200, {})
  } catch (error) {
    console.error("stop-recoding error:", error);
  }
});

3)轮询机器人状态

/*
* Gets the current state of the Recall Bot
*/
server.get('/recording-state', async (req, res) => {
  try {

      const botId = local_botId;

      if (!botId) {
          res.send(400, JSON.stringify({ error: 'Missing botId' }));
      }

      const response = await fetch(`https://us-west-2.recall.ai/api/v1/bot/${botId}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Token ${RECALL_API_KEY}`
          },
      });
      const bot = await response.json();
      const latestStatus = bot.status_changes.slice(-1)[0].code;

      console.log('state:', latestStatus);

      res.send(200, JSON.stringify({
          state: latestStatus,
          transcript: db.transcripts[botId] || [],
      }));
  } catch (error) {
    console.error("recoding-state error:", error);
  }
});

4)接收转写存储在db中(本例使用的是内存)

/*
 * Receives transcription webhooks from the Recall Bot
 */
server.post('/transcription', async (req, res) => {
  try {
      console.log('transcription webhook received: ', req.body);

      const { bot_id, transcript } = req.body.data;

      if (!db.transcripts[bot_id]) {
          db.transcripts[bot_id] = [];
      }
      if (transcript)
      {
        db.transcripts[bot_id].push(transcript);
      }

      res.send(200, JSON.stringify({ success: true }));

  } catch (error) {
    console.error("transcription error:", error);
  }
});

完整的服务端代码:

import restify from "restify";
import send from "send";
import fs from "fs";
import fetch from "node-fetch";
import path from 'path';
import { fileURLToPath } from 'url';
import { storeToken, getToken } from './redisClient.js';
import { WebSocketServer, WebSocket } from 'ws';

const __filename = fileURLToPath(import.meta.url);
console.log('__filename: ', __filename);

const __dirname = path.dirname(__filename);
console.log('__dirname: ', __dirname);

// Create HTTP server.
const server = restify.createServer({
  key: process.env.SSL_KEY_FILE ? fs.readFileSync(process.env.SSL_KEY_FILE) : undefined,
  certificate: process.env.SSL_CRT_FILE ? fs.readFileSync(process.env.SSL_CRT_FILE) : undefined,
  formatters: {
    "text/html": function (req, res, body) {
      return body;
    },
  },
});

server.use(restify.plugins.bodyParser());
server.use(restify.plugins.queryParser());

server.get(
  "/static/*",
  restify.plugins.serveStatic({
    directory: __dirname,
  })
);

server.listen(process.env.port || process.env.PORT || 3000, function () {
  console.log(`\n${server.name} listening to ${server.url}`);
});

// Adding tabs to our app. This will setup routes to various views
// Setup home page
server.get("/config", (req, res, next) => {
  send(req, __dirname + "/config/config.html").pipe(res);
});

// Setup the static tab
server.get("/meetingTab", (req, res, next) => {
  send(req, __dirname + "/panel/panel.html").pipe(res);
});

//获得用户token
server.get('/auth', (req, res, next) => {
  res.status(200);
  res.send(`
<!DOCTYPE html>
<html>
<head>
    <script>
        // Function to handle the token storage
        async function handleToken() {
            const hash = window.location.hash.substring(1);
            const hashParams = new URLSearchParams(hash);
            const access_token = hashParams.get('access_token');
            console.log('Received hash parameters:', hashParams);

            if (access_token) {
                console.log('Access token found:', access_token);
                localStorage.setItem("access_token", access_token);
                console.log('Access token stored in localStorage');
                try {
                  const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/store_user_token', {
                      method: 'POST',
                      headers: {
                          'Content-Type': 'application/json'
                      },
                      body: JSON.stringify({ "user_token" : access_token })
                  });

                  if (response.ok) {
                      console.log('Token stored successfully');
                  } else {
                      console.error('Failed to store token:', response.statusText);
                  }
              } catch (error) {
                  console.error('Error storing token:', error);
              }
            } else {
                console.log('No access token found');
            }
            window.close();
        }

        // Call the function to handle the token
        handleToken();
    </script>
</head>
<body></body>
</html>
  `);
  next();
});

// 存储 user_token
server.post('/store_user_token', async (req, res) => {
  const user_token = req.body.user_token;
  if (!user_token) {
      res.status(400);
      res.send('user_token are required');
  }
  try {
      // Store user token
      await storeToken('user_token', user_token);
      console.log('user_token stored in Redis');
  } catch (err) {
      console.error('user_token store Error:', err);
  }
  res.status(200);   
  res.send('Token stored successfully');
});

// 获取 user_token
server.get('/get_user_token', async (req, res) => {
  try {
    // Store user token
    const user_token = await getToken('user_token');
    console.log('user_token get in Redis');
    res.send({"user_token": user_token});
  } catch (err) {
      console.error('user_token get Error:', err);
  }
});

//应用token
let app_token = '';
const app_token_refresh_interval = 3000 * 1000; // 3000秒

const getAppToken = async () => {
  try {
    // 构建请求体
    const requestBody = new URLSearchParams({
      "grant_type": "client_credentials",
      "client_id": "Azure注册应用ID",
      "client_secret": "Azure注册应用密钥",
      "scope": "https://graph.microsoft.com/.default",
    }).toString();

    // 获取app令牌
    const tokenUrl = `https://login.microsoftonline.com/864168b4-813c-411a-827a-af408f70c665/oauth2/v2.0/token`;
    const tokenResponse = await fetch(tokenUrl, {
      method: 'POST',
      headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: requestBody,
    });

    if (!tokenResponse.ok) {
      const errorData = await tokenResponse.json();
      throw new Error(errorData.error_description);
    }

    const tokenData = await tokenResponse.json();
    app_token = tokenData.access_token;
    console.log("app_token received!");
  } catch (error) {
    console.error('Error getting app token:', error);
  }
};

// 定期刷新 app_token
setInterval(getAppToken, app_token_refresh_interval);

// 确保在服务器启动时获取 app_token
getAppToken();

//存储机器人转写信息
const db = {
  transcripts: {
      // [bot id]: [transcript]
  },
};

const RECALL_API_KEY = '你的recall.ai的API KEY';
const WEBHOOK_SECRET = '在recall.ai配置webhook端点时的密钥';

let local_botId = null;
/*
 * Send's a Recall Bot to start recording the call
 */
server.post('/start-recording', async (req, res) => {
  const meeting_url = req.body.meetingUrl;
  try {
      if (!meeting_url) {
          return res.status(400).json({ error: 'Missing meetingUrl' });
      }

      console.log('recall bot start recording', meeting_url);

      const url = 'https://us-west-2.recall.ai/api/v1/bot/';
      const options = {
        method: 'POST',
        headers: {
          accept: 'application/json',
          'content-type': 'application/json',
          Authorization: `Token ${RECALL_API_KEY}`
        },
        body: JSON.stringify({
          bot_name: 'teams bot',
          real_time_transcription: {
            destination_url: 'https://shortly-adapted-akita.ngrok-free.app/transcription?secret=' + WEBHOOK_SECRET,
            partial_results: false
          },
          transcription_options: {provider: 'assembly_ai'},
          meeting_url: meeting_url
        })
      };

      const response = await fetch(url, options);
      const bot = await response.json();
      local_botId = bot.id
      console.log('botId:', local_botId);

      res.send(200, JSON.stringify({
          botId: local_botId
      }));
  } catch (error) {
    console.error("start-recoding error:", error);
  }
});

/*
* Tells the Recall Bot to stop recording the call
*/
server.post('/stop-recording', async (req, res) => {
  try {
      const botId = local_botId;
      if (!botId) {
          res.send(400, JSON.stringify({ error: 'Missing botId' }));
      }
      await fetch(`https://us-west-2.recall.ai/api/v1/bot/${botId}/leave_call`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Token ${RECALL_API_KEY}`
          },
      });
      console.log('recall bot stopped');
      res.send(200, {})
  } catch (error) {
    console.error("stop-recoding error:", error);
  }
});

/*
* Gets the current state of the Recall Bot
*/
server.get('/recording-state', async (req, res) => {
  try {

      const botId = local_botId;

      if (!botId) {
          res.send(400, JSON.stringify({ error: 'Missing botId' }));
      }

      const response = await fetch(`https://us-west-2.recall.ai/api/v1/bot/${botId}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            Authorization: `Token ${RECALL_API_KEY}`
          },
      });
      const bot = await response.json();
      const latestStatus = bot.status_changes.slice(-1)[0].code;

      console.log('state:', latestStatus);

      res.send(200, JSON.stringify({
          state: latestStatus,
          transcript: db.transcripts[botId] || [],
      }));
  } catch (error) {
    console.error("recoding-state error:", error);
  }
});
/*
 * Receives transcription webhooks from the Recall Bot
 */
server.post('/transcription', async (req, res) => {
  try {
      console.log('transcription webhook received: ', req.body);

      const { bot_id, transcript } = req.body.data;

      if (!db.transcripts[bot_id]) {
          db.transcripts[bot_id] = [];
      }
      if (transcript)
      {
        db.transcripts[bot_id].push(transcript);
      }

      res.send(200, JSON.stringify({ success: true }));

  } catch (error) {
    console.error("transcription error:", error);
  }
});

二、页面需要实现开始录音和停止录音按钮及转写显示。

完整的页面代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Meeting Transcripts</title>
    <script src="https://res.cdn.office.net/teams-js/2.0.0/js/MicrosoftTeams.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
    <style>
        .subtitle {
            display: flex;
            align-items: center;
            margin-bottom: 10px;
        }
        .speaker-photo {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            margin-right: 10px;
        }
        button {
            padding: 5px 10px; /* 调整按钮的 padding 以减小高度 */
            font-size: 14px; /* 调整按钮的字体大小 */
            margin-right: 10px;
        }
        #transcript {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ccc;
            min-height: 100px;
            width: 100%;
        }
    </style>
</head>
<body>
    <h2>Meeting Transcripts</h2>
    <button id="startRecording">Start Recording</button>
    <button id="stopRecording" disabled>Stop Recording</button>
    <div id="transcripts"></div>

    <script>
        const clientId = 'Azure注册应用ID';
        const tenantId = 'Azure注册应用租户ID';
        const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`;
        const redirectUri = 'https://shortly-adapted-akita.ngrok-free.app/auth'; // 确保与服务器端一致
        const scope = 'user.read';

        let user_token = null;
        let meetingOrganizerUserId = null;
        let participants = {}; // 用于存储参会者的信息
        let userPhotoCache = {}; // 用于缓存用户头像
        let tokenFetched = false; // 标志变量,用于跟踪是否已经获取了 user_token
        let displayedTranscriptIds = new Set(); // 用于存储已经显示的转录片段的 ID

        const getUserInfo = async (userId, accessToken) => {
            const graphUrl = `https://graph.microsoft.com/v1.0/users/${userId}`;
            const response = await fetch(graphUrl, {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });

            if (response.status === 401) {
                // 如果 token 超期,重新触发 initAuthentication
                initAuthentication();
                return null;
            }

            const userInfo = await response.json();
            return userInfo;
        };

        const getUserPhoto = async (userId, accessToken) => {
            if (userPhotoCache[userId]) {
                return userPhotoCache[userId];
            }

            const graphUrl = `https://graph.microsoft.com/v1.0/users/${userId}/photo/$value`;
            const response = await fetch(graphUrl, {
                headers: {
                    'Authorization': `Bearer ${accessToken}`
                }
            });

            if (!response.ok) {
                const errorData = await response.json();
                console.error('Error fetching user photo:', errorData);
                return null;
            }

            const photoBlob = await response.blob();
            const photoUrl = URL.createObjectURL(photoBlob);
            userPhotoCache[userId] = photoUrl; // 缓存头像 URL
            return photoUrl;
        };

        const getMeetingDetails = async (user_token, joinMeetingId) => {
            const apiUrl = `https://graph.microsoft.com/v1.0/me/onlineMeetings?$filter=joinMeetingIdSettings/joinMeetingId eq '${joinMeetingId}'`;
            const response = await fetch(apiUrl, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${user_token}`,
                    'Content-Type': 'application/json'
                }
            });
        
            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(`getMeetingDetails status: ${response.status}, message: ${errorData.error}`);
            }
        
            const data = await response.json();
            return data.value[0];
        };

        const getTranscriptContent = async (transcripts) => {
            const subtitles = [];
            try {
                transcripts.forEach(transcript => {
                    const startTime = transcript.words[0].start_time;
                    const endTime = transcript.words[transcript.words.length - 1].end_time;
                    const speaker = transcript.speaker;
                    const content = transcript.words.map(word => word.text).join(' ');
                    subtitles.push({ startTime, endTime, speaker, content, id: transcript.original_transcript_id });
                });
                return subtitles;
            } catch (error) {
                console.error('getTranscriptContent error:', error);
                return subtitles;
            }
        };

        const displaySubtitle = async (subtitle, transcriptElement, accessToken) => {
            const subtitleElement = document.createElement('div');
            subtitleElement.classList.add('subtitle');

            // 获取说话者的头像
            const speakerUserId = participants[subtitle.speaker];
            const speakerPhotoUrl = speakerUserId ? await getUserPhoto(speakerUserId, accessToken) : 'default-avatar.png';

            // 创建头像元素
            const speakerPhotoElement = document.createElement('img');
            speakerPhotoElement.src = speakerPhotoUrl;
            speakerPhotoElement.alt = subtitle.speaker;
            speakerPhotoElement.classList.add('speaker-photo');

            // 创建输出字符串
            const output = `${subtitle.startTime} - ${subtitle.endTime}\n${subtitle.content}`;
            
            subtitleElement.appendChild(speakerPhotoElement);
            subtitleElement.appendChild(document.createTextNode(output));
            transcriptElement.appendChild(subtitleElement);
        };

        const init = async () => {
            try {
                if (!tokenFetched) {
                    const response = await fetch('https://shortly-adapted-akita.ngrok-free.app/get_user_token');
                    const data = await response.json();
                    if (response.ok) {
                        user_token = data.user_token;
                        console.log('user token retrieved:', user_token);
                        tokenFetched = true;
                    } else {
                        console.error('Failed to get token:', response.statusText);
                        return;
                    }
                }
                console.log('User Token:', user_token);
                const joinMeetingId = '45756456529'; // 替换为你要查询的 joinMeetingId
                try {
                    const meetingDetails = await getMeetingDetails(user_token, joinMeetingId);
                    console.log('Meeting Details:', meetingDetails);
                    meetingOrganizerUserId = meetingDetails.participants.organizer.identity.user.id;
                    const meetingId = meetingDetails.id; // 获取会议 ID
                    console.log('Organizer User ID:', meetingOrganizerUserId);
                    console.log('Meeting ID:', meetingId);
                    // 获取主持人信息
                    const organizerInfo = await getUserInfo(meetingOrganizerUserId, user_token);
                    const organizerDisplayName = organizerInfo.displayName;
                    participants[organizerDisplayName] = meetingOrganizerUserId;
                    // 获取参会者信息
                    const attendeesPromises = meetingDetails.participants.attendees.map(async attendee => {
                        const userId = attendee.identity.user.id;
                        const userInfo = await getUserInfo(userId, user_token);
                        const displayName = userInfo.displayName;
                        participants[displayName] = userId;
                    });
                    await Promise.all(attendeesPromises);
                } catch (error) {
                    console.error('Error fetching meeting details:', error);
                }
            } catch (error) {
                console.error('Error getting token:', error);
            }
        };

        const initAuthentication = () => {
            microsoftTeams.app.initialize();
            microsoftTeams.authentication.authenticate({
                url: `${authUrl}?client_id=${clientId}&response_type=token&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`,
                width: 600,
                height: 535,
                successCallback: async (result) => {
                    console.log('Authentication success:', result);
                },
                failureCallback: (error) => {
                    console.error('Authentication failed:', error);
                }
            });
        };

        // 设置较长的轮询时间来防止 user_token 的超期
        setInterval(initAuthentication, 3000000); // 每3000秒(50分钟)轮询一次
        initAuthentication();
        init();

        // 录音控制功能
        const startRecordingButton = document.getElementById('startRecording');
        const stopRecordingButton = document.getElementById('stopRecording');
        const transcriptDiv = document.getElementById('transcript');

        let recordingInterval;

        // Function to start recording
        async function startRecording() {
            const meetingUrl = await getMeetingUrl();
            if (!meetingUrl) return;

            try {
                const response = await fetch('/start-recording', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ meetingUrl }),
                });

                if (response.ok) {
                    const data = await response.json();
                    console.log('Bot started:', data);
                    startRecordingButton.disabled = true;
                    stopRecordingButton.disabled = false;
                    startPolling();
                } else {
                    console.error('Failed to start recording:', response.statusText);
                }
            } catch (error) {
                console.error('Error starting recording:', error);
            }
        }

        // Function to stop recording
        async function stopRecording() {
            try {
                const response = await fetch('/stop-recording', {
                    method: 'POST',
                });

                if (response.ok) {
                    console.log('Bot stopped');
                    startRecordingButton.disabled = false;
                    stopRecordingButton.disabled = true;
                    clearInterval(recordingInterval);
                } else {
                    console.error('Failed to stop recording:', response.statusText);
                }
            } catch (error) {
                console.error('Error stopping recording:', error);
            }
        }

        // Function to poll the recording state
        async function pollRecordingState() {
            try {
                const response = await fetch('/recording-state');
                if (response.ok) {
                    const data = await response.json();
                    updateUI(data);
                } else {
                    console.error('Failed to get recording state:', response.statusText);
                }
            } catch (error) {
                console.error('Error polling recording state:', error);
            }
        }

        // Function to update the UI based on the recording state
        function updateUI(data) {
            const { state, transcript } = data;
            console.log(state, transcript);
            // Update the transcript display
            const transcriptsContainer = document.getElementById('transcripts');
            const transcriptElement = document.createDocumentFragment(); // 使用 DocumentFragment 优化 DOM 操作
            if (transcript.length > 0) {
                getTranscriptContent(transcript)
                    .then(subtitles => {
                        subtitles.forEach(subtitle => {
                            if (!displayedTranscriptIds.has(subtitle.id)) {
                                displaySubtitle(subtitle, transcriptElement, user_token);
                                displayedTranscriptIds.add(subtitle.id); // 添加到已显示的转录片段 ID 集合中
                            }
                        });
                    })
                    .catch(error => {
                        const errorElement = document.createElement('div');
                        errorElement.innerHTML = `<strong>${error}</strong>`;
                        transcriptElement.appendChild(errorElement);
                    })
                    .finally(() => {
                        transcriptsContainer.appendChild(transcriptElement); // 一次性插入 DOM
                    });
            }

            // Update button states based on the recording state
            if (state === 'recording') {
                startRecordingButton.disabled = true;
                stopRecordingButton.disabled = false;
            } else if (state === 'stopped') {
                startRecordingButton.disabled = false;
                stopRecordingButton.disabled = true;
            }
        }

        // Function to start polling the recording state every 2 seconds
        function startPolling() {
            recordingInterval = setInterval(pollRecordingState, 2000);
        }

        // Event listeners for buttons
        startRecordingButton.addEventListener('click', startRecording);
        stopRecordingButton.addEventListener('click', stopRecording);

        // Function to get the meeting URL from the meeting details
        async function getMeetingUrl() {
            const joinMeetingId = '45756456529'; // 替换为你要查询的 joinMeetingId
            try {
                const meetingDetails = await getMeetingDetails(user_token, joinMeetingId);
                return meetingDetails.joinWebUrl;
            } catch (error) {
                console.error('Error fetching meeting URL:', error);
                return null;
            }
        }
    </script>
</body>
</html>

最终效果:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实战教程!Zabbix 监控 Spark 中间件配置教程

本文将介绍以JMX方式监控Spark中间件。JMX具有跨平台、灵活性强、监控能力强、易于集成与扩展、图形化界面支持以及安全性与可配置性等多方面的优势&#xff0c;是监控Spark等复杂Java应用程序的重要工具之一。 Apache Spark 是一个开源的大数据处理框架&#xff0c;它提供了快…

【深度学习】ubuntu系统下docker部署cvat的自动标注功能(yolov8 segmentation)

cvat部署自动标注教程 前言step1. 拷贝yolov8项目step2. 创建yolov8的本地镜像step3. 在cvat中构建我们的工作空间 前言 安装docker和cvat的流程我这里就不赘述了&#xff0c;这样的教程还是挺多的&#xff0c;但是对于使用docker在cvat上部署自动标注算法的整个详细流程&#…

【MySQL】MVCC及其实现原理

目录 1. 概念介绍 什么是MVCC 什么是当前读和快照读 MVCC的好处 2. MVCC实现原理 隐藏字段 Read View undo-log 数据可见性算法 3. RC和RR隔离级别下MVCC的差异 4. MVCC&#xff0b;Next-key-Lock 防止幻读 1. 概念介绍 什么是MVCC Multi-Version Concurrency Cont…

通信工程学习:什么是FDD频分双工

FDD:频分双工 FDD(频分双工,Frequency Division Duplexing)是一种无线通信技术,它通过将频谱划分为上行和下行两个不重叠的频段来实现同时双向通信。以下是FDD频分双工的详细解释: 一、定义与原理 定义: FDD是一种无线通信系统的工作模式,其中上行链路(从移动…

以Flask为基础的虾皮Shopee“曲线滑块验证码”识别系统部署

以Flask为基础的虾皮Shopee“曲线滑块验证码”识别系统部署 一、验证码类型二、简介三、Flask应用 一、验证码类型 验证码类型&#xff1a;此类验证码存在两个难点&#xff0c;一是有右侧有两个凹槽&#xff0c;二是滑块的运动轨迹不是直线的&#xff0c;而是沿着曲线走的&…

您的业​​务端点是否完全安全?

根据 2023 年数据泄露调查报告&#xff0c;52% 的数据泄露涉及凭证泄露。这令人担忧&#xff0c;不是吗&#xff1f; 在当今的数字世界中&#xff0c;企业严重依赖技术&#xff0c;保护您的设备&#xff08;端点&#xff09;至关重要。这些设备&#xff08;包括计算机、笔记本…

MySQL从入门到精通 - 基础篇

一、MySQL概述 1. 数据库相关概念 二、SQL &#xff08;1&#xff09;SQL通用语法 &#xff08;2&#xff09;SQL分类 &#xff08;3&#xff09;数据定义语言DDL 数据库操作 表操作 数据类型 1. 数值类型 2. 字符串类型 二进制数据&#xff1a;以二进制格式&#xff08;0和…

uniapp 知识点

自定义导航 在page.json navigationstyle":"custom"navigateTo传参 页面传参只能onLoad(option)里面拿 px和upx的关系 在750设计图中&#xff0c;1px1upx 路由 navigateBack返回上一页 重定向 其实就是把当前页面干掉了 公共组件和页面共同点 computed,watc…

基于微信小程序的智能汽车充电站系设计与实现(源码+定制+文档)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

Spring Boot技术:构建高效网上购物平台

第3章 系统分析 3.1 可行性分析 在系统开发之初要进行系统可行分析&#xff0c;这样做的目的就是使用最小成本解决最大问题&#xff0c;一旦程序开发满足用户需要&#xff0c;带来的好处也是很多的。下面我们将从技术上、操作上、经济上等方面来考虑这个系统到底值不值得开发。…

【Vue】Vue3 的初始化过程

核心流程是patch&#xff0c;然后Patch有一个分支&#xff0c;分别处理组件和浏览器原生标签。分别对应processElement和processComponent&#xff0c;从上到下插入&#xff0c;知道处理完成&#xff0c;才把顶层div插入到浏览器。“一次性渲染&#xff0c;而不是一个个一个渲染…

[论文笔记] Chain-of-Thought Reasoning without Prompting

分析: 在CoT解码路径中,我们可以看到模型在第三个位置(𝑖? = 3)开始展示推理过程,并且给出了正确的答案“8”。模型首先识别出说话者有3个苹果,然后识别出爸爸比说话者多2个,即5个苹果,最后将这两个数量相加得到总数8个苹果。 这个例子表明,通过探索替代的解码路径…

【每天学个新注解】Day 7 Lombok注解简解(六)—@With

With 创建一个新的对象&#xff0c;该对象是当前对象的副本&#xff0c;但某些字段的值可以被更改。 1、如何使用 With 可以使用在类上&#xff0c;也可以使用在成员变量上。加在类上相当于给所有成员变量 With可以配合AccessLevel使用&#xff0c;创建出指定访问修饰符的wi…

多模态大模型学习(一)

参考&#xff1a;https://www.bilibili.com/video/BV1kT411o7a6?p2&spm_id_frompageDriver&vd_source156234c72054035c149dcb072202e6be 余弦相似度&#xff0c;让正样本内积趋近于1&#xff0c;负样本趋近于-1。度量学习。N特别大时&#xff0c;负样本远大于正样本&…

PHP之 实现https ssl证书到期提醒,通过企微发送消息

参考文章 https://blog.51cto.com/17099933344/1935194 https://blog.csdn.net/m0_37346206/article/details/127333463 https://www.cnblogs.com/tk-bolg/p/18108106 使用的企微接口 https://qyapi.weixin.qq.com/cgi-bin/message/send 查询 ssl证书到期时间 // ssl证书即将…

基于BiLSTM+Transformer混合模型实现交通流量时序预测(PyTorch版)

前言 系列专栏:【深度学习&#xff1a;算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域&#xff0c;讨论了各种复杂的深度神经网络思想&#xff0c;如卷积神经网络、循环神经网络、生成对…

2024年研究生数学建模“华为杯”E题——肘部法则、k-means聚类、目标检测(python)、ARIMA、逻辑回归、混淆矩阵(附:目标检测代码)

文章目录 一、情况介绍二、思路情况二、代码展示三、感受 一、情况介绍 前几天也是参加了研究生数学建模竞赛&#xff08;也就是华为杯&#xff09;&#xff0c;也是和本校的两个数学学院的朋友在网上组的队伍。昨天&#xff08;9.25&#xff09;通宵干完论文&#xff08;一条…

Windows安装openssl开发库

1 下载openssl安装包并安装 下载网址&#xff1a; https://slproweb.com/products/Win32OpenSSL.html 下载对应的安装版本。 双击安装包&#xff0c;一路下一步完成安装。注意&#xff1a;1.安装路径不要有空格&#xff1b; 2. 建议不要把DLL拷贝到系统路径。 2 编辑代码 …

“类型名称”在Go语言规范中的演变

Go语言规范&#xff08;The Go Programming Language Specification&#xff09;[1]是Go语言的核心文档&#xff0c;定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准&#xff0c;它确保了语言的稳定性和一致性&#…

制造企业为何需要PLM系统?PLM系统解决方案对制造业重要性分析

制造企业为何需要PLM系统&#xff1f;PLM系统解决方案对制造业重要性分析 新华社9月23日消息&#xff0c;据全国组织机构统一社会信用代码数据服务中心统计&#xff0c;我国制造业企业总量突破600万家。数据显示&#xff0c;2024年1至8月&#xff0c;我国制造业企业数量呈现稳…