import {
v4 as uuidv4
} from 'uuid';
var OpenCC = require("opencc-js");
let converter = OpenCC.Converter({
from: "hk",
to: "cn"
});
import _ from "lodash";
import Papa from "papaparse";
import markdownIt from "markdown-it";
export const toolMixin = {
methods: {
testMixin() {
},
getTextContentMixin(file) {
return new Promise((resolve) => {
const txtRegex = /\.(txt|csv)$/i;
if (txtRegex.test(file.url)) {
this.getFile(file.url).then(fileData => {
const arr = Papa.parse(fileData.data, {
header: false
}).data.slice(1);
// console.log(arr)
const _editorBarDataContent = `
序号 |
开始时间 |
结束时间 |
发言内容 |
时长 |
说话人身份 |
行为编码 |
${arr.map(row => `
${_.get(row, 0, "")} |
${_.get(row, 1, "")} |
${_.get(row, 2, "")} |
${_.get(row, 3, "")} |
${_.get(row, 4, "")} |
${_.get(row, 5, "")} |
${_.get(row, 6, "")} |
`).join("\n")}
`;
var blob = new Blob([_editorBarDataContent], { type: "text/plain;charset=utf-8" });
blob.lastModifiedDate = new Date();
blob.name = `${file.name.replace('.txt', '')}_classroomObservation.txt`;
this.uploadFileMixin(blob).then(upload => {
resolve({ editorBarData: { type: "0", url: upload.Location, content: _editorBarDataContent } })
// this.ajax
// .put("https://gpt4.cocorobo.cn/upload_file_knowledge", {
// url: upload.Location
// })
// .then(res => {
// let resData = res.data.FunctionResponse;
// if (resData.result && resData.result.id) {
// resolve({ fileId: resData.result.id, editorBarData: { type: "0", url: upload.Location }, })
// }
// })
})
})
}
})
},
uploadFileMixin(file) {
return new Promise((resolve, reject) => {
var credentials = {
accessKeyId: "AKIATLPEDU37QV5CHLMH",
secretAccessKey: "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"
}; //秘钥形式的登录上传
window.AWS.config.update(credentials);
window.AWS.config.region = "cn-northwest-1"; //设置区域
var bucket = new window.AWS.S3({ params: { Bucket: "ccrb" } }); //选择桶
var _this = this;
if (file) {
var params = {
Key:
file.name.split(".")[0] +
new Date().getTime() +
"." +
file.name.split(".")[file.name.split(".").length - 1],
ContentType: file.type,
Body: file,
"Access-Control-Allow-Credentials": "*",
ACL: "public-read"
}; //key可以设置为桶的相抵路径,Body为文件, ACL最好要设置
var options = {
partSize: 2048 * 1024 * 1024,
queueSize: 2,
leavePartsOnError: true
};
bucket
.upload(params, options)
.on("httpUploadProgress", function (evt) {
//这里可以写进度条
// _this.progressData.value = parseInt((evt.loaded * 100) / evt.total);
// console.log("Uploaded : " + parseInt((evt.loaded * 80) / evt.total) + '%');
})
.send(function (err, data) {
if (err) {
_this.$message.error("上传失败");
} else {
resolve(data);
}
});
}
})
},
getFileIdMixin(url) {
return new Promise((resolve) => {
this.ajax
.put("https://gpt4.cocorobo.cn/upload_file_knowledge", {
url: url
})
.then(res => {
let resData = res.data.FunctionResponse;
if (resData.result && resData.result.id) {
resolve({ fileId: resData.result.id })
}
})
})
},
getVideoToVoiceAndUploadMixin(fileData) {
return new Promise(async (resolve) => {
let _file = null;
console.log("fileData👉", fileData)
if (fileData.fileObj) {
_file = fileData.fileObj
} else if (fileData.url) {
let videoRes = await this.getFileBody(fileData.url);
if (videoRes.data === 1) return resolve({ data: 1 })
// 把uint8Array转换为视频文件
_file = new File([videoRes.data], 'video.mp4', { type: 'video/mp4' });
}
if (!_file) return resolve({ data: 2,err:"未找到文件" })
console.log("需要处理的文件👉", _file)
try {
const reader = new FileReader();
reader.onload = async (e) => {
try {
// 创建音频上下文
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
//解码音频数据
const buffer = await audioContext.decodeAudioData(e.target.result);
//创建离线音频上下文
const offlineAudioContext = new OfflineAudioContext({ numberOfChannels: buffer.numberOfChannels, length: buffer.length, sampleRate: buffer.sampleRate });
//创建音源节点
const source = offlineAudioContext.createBufferSource();
source.buffer = buffer;
source.connect(offlineAudioContext.destination);
source.start();
//渲染音频
const renderedBuffer = await offlineAudioContext.startRendering();
const wavBlob = this.bufferToWav(renderedBuffer);
// blob转成file文件
const audioFile = new File([wavBlob], 'audio.wav', { type: 'audio/wav' });
this.uploadFileMixin(audioFile).then(upload => {
resolve({ audioUrl: upload, fileObj: audioFile })
})
} catch (error) {
console.log("👉", error);
return resolve({ data: 2,err:error })
}
}
reader.readAsArrayBuffer(_file);
} catch (error) {
console.log("👉", error);
return resolve({ data: 2,err:error })
}
})
},
bufferToWav(audioBuffer) {
const numOfChan = audioBuffer.numberOfChannels;
const length = audioBuffer.length * numOfChan * 2;
const buffer = new ArrayBuffer(44 + length);
const view = new DataView(buffer);
const channels = [];
let pos = 0;
// 获取通道数据
for (let i = 0; i < audioBuffer.numberOfChannels; i++) {
channels.push(audioBuffer.getChannelData(i));
}
// 写入WAV头
this.writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 44 + length - 8, true);
this.writeUTFBytes(view, 8, 'WAVE');
this.writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, numOfChan, true);
view.setUint32(24, audioBuffer.sampleRate, true);
view.setUint32(28, audioBuffer.sampleRate * 2 * numOfChan, true);
view.setUint16(32, numOfChan * 2, true);
view.setUint16(34, 16, true);
this.writeUTFBytes(view, 36, 'data');
view.setUint32(40, length, true);
// 写入PCM数据
pos = 44;
for (let i = 0; i < audioBuffer.length; i++) {
for (let j = 0; j < numOfChan; j++) {
const sample = Math.max(-1, Math.min(1, channels[j][i]));
view.setInt16(pos, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
pos += 2;
}
}
return new Blob([buffer], { type: 'audio/wav' });
},
writeUTFBytes(view, offset, string) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
},
getFile(url) {
return new Promise((resolve, reject) => {
var credentials = {
accessKeyId: "AKIATLPEDU37QV5CHLMH",
secretAccessKey: "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"
}; //秘钥形式的登录上传
window.AWS.config.update(credentials);
window.AWS.config.region = "cn-northwest-1"; //设置区域
let url2 = url;
let _url2 = "";
if (
url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
) {
_url2 = url2.split(
"https://view.officeapps.live.com/op/view.aspx?src="
)[1];
} else {
_url2 = url2;
}
var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
let name = decodeURIComponent(
_url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1]
);
var params = {
Bucket: "ccrb",
Key: name
};
s3.getObject(params, function (err, data) {
if (err) {
console.log(err, err.stack);
resolve({ data: 1 });
} else {
const fileContent = data.Body.toString("utf-8");
resolve({ data: fileContent });
} // sxuccessful response
});
// axios({
});
},
getFileBody(url) {
return new Promise((resolve, reject) => {
var credentials = {
accessKeyId: "AKIATLPEDU37QV5CHLMH",
secretAccessKey: "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",
}; //秘钥形式的登录上传
window.AWS.config.update(credentials);
window.AWS.config.region = "cn-northwest-1"; //设置区域
let url2 = url;
let _url2 = "";
if (
url2.indexOf("https://view.officeapps.live.com/op/view.aspx?src=") != -1
) {
_url2 = url2.split(
"https://view.officeapps.live.com/op/view.aspx?src="
)[1];
} else {
_url2 = url2;
}
var s3 = new window.AWS.S3({ params: { Bucket: "ccrb" } });
let name = decodeURIComponent(_url2.split("https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/")[1])
var params = {
Bucket: "ccrb",
Key: name
};
s3.getObject(params, function (err, data) {
if (err) {
console.log(err, err.stack)
resolve({ data: 1 });
} else {
resolve({ data: data.Body });
console.log(data);
} // sxuccessful response
});
});
},
getAnalysisMixin(obj) {
return new Promise(async (resolve) => {
let { fileId, assistantData, content, analysisData, baseMessage } = obj;
let type = 0;
let tips = ""
analysisData.mId = assistantData.id;
// console.log("处理数据👉", fileId, assistantData, content, analysisData, baseMessage)
if (['f8795150-699c-11ef-b873-005056b86db5', '01928d2b-699d-11ef-b873-005056b86db5', '069af7b9-699d-11ef-b873-005056b86db5', 'bfe844b1-7a45-11ef-9b30-005056b86db5'].includes(assistantData.id)) {//S-T分析:课堂时间分配 S-T分析:师生互动分析 S-T分析:教学模式分析 课堂活动光谱图
try {
let _result = [];
let _data = content;
let _div = document.createElement("div");
_div.innerHTML = _data;
let _tableRows = _div.querySelectorAll(`table tbody tr`);
_tableRows.forEach((i, index) => {
if (index == 0) return;
let obj = {
index: i.cells[0].textContent,
startTime: i.cells[1].textContent,
endTime: i.cells[2].textContent,
message: i.cells[3].textContent,
time: i.cells[4].textContent,
role: i.cells[5] ? i.cells[5].textContent : "",
behavior: i.cells[6] ? i.cells[6].textContent : ""
};
_result.push(obj);
});
if (_result.length == 0) return resolve({ data: 1, err: "未找到表格数据" });
if (assistantData.id == "f8795150-699c-11ef-b873-005056b86db5") {//课堂时间分配
let resultData = await this.getTimeAllocationDataMixin(_result, fileId)
if (resultData.message) analysisData.content = resultData.message;
if (resultData.eCharts) analysisData.eChartData = resultData.eCharts;
return resolve({ data: analysisData })
} else if (assistantData.id == "01928d2b-699d-11ef-b873-005056b86db5") {//师生互动分析
let resultData = await this.getInteractionAnalysisData(_result, fileId)
if (resultData.message) analysisData.content = resultData.message;
if (resultData.eCharts) analysisData.eChartData = resultData.eCharts;
return resolve({ data: analysisData })
} else if (assistantData.id == "069af7b9-699d-11ef-b873-005056b86db5") {//教学模式分析
let resultData = await this.getTeachingModeData(_result, fileId)
if (resultData.message) analysisData.content = resultData.message;
if (resultData.RT) analysisData.RT = resultData.RT;
if (resultData.CH) analysisData.CH = resultData.CH;
return resolve({ data: analysisData })
} else if (assistantData.id == "bfe844b1-7a45-11ef-9b30-005056b86db5") {//课堂活动光谱图
let resultData = await this.getSpectrogram(_result, fileId, content, assistantData)
if (resultData.message) analysisData.content = resultData.message;
if (resultData.eCharts) analysisData.getSpectrogram = resultData.eCharts;
return resolve({ data: analysisData })
}
} catch (error) {
return resolve({ data: 1, err: err })
}
} else {
let _msg = `使用文件检索的方式完整的去分析文件内容,并请完全按照要求输出。`;
if (assistantData.tips) {
tips = assistantData.tips;
type = 1;
} else if (assistantData.agentid) {
type = 0;
}
if (assistantData.id === '6b4a9650-48be-11ef-936b-12e77c4cb76b') {
_msg = `使用文件检索的方式完整的去分析文件内容,并基于以下的课堂基本内容,使用cpote课程设计模型改编一堂同主题的课程。
课堂名称:${baseMessage.courseName} 搜课年级:${baseMessage.grade} 授课科目:${baseMessage.subject}`;
}
let params = {
id:
type == 0
? assistantData.agentid
: "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
message: type == 0 ? _msg : tips,
session_name: uuidv4(),
userId: this.userId,
file_ids: fileId ? [fileId] : [],
model: "gpt-4o-2024-11-20",
sound_url: "",
temperature: 0.2,
top_p: 1,
max_completion_tokens: 4096,
stream: false,
uid: uuidv4()
};
this.ajax
.post("https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat", params)
.then(async res => {
let _data = res.data;
analysisData.content = _data.message;
if (['1', '2', '3'].includes(assistantData.echartsType)) {
let echartsData = await this.getEChartsDataMixin(assistantData.echartsType, _data.message)
if (echartsData.data == 1) {
console.log(`生成表格失败${echartsData.err}`)
resolve({ data: analysisData })
} else {
analysisData.eChartData = echartsData.data;
resolve({ data: analysisData })
}
} else {
resolve({ data: analysisData })
}
})
.catch(err => {
resolve({ data: 1, err: err })
});
}
})
},
getContentTableMixin(content) {
return new Promise(resolve => {
let _content = content;
const md = new markdownIt();
let _contentHtml = md.render(_content);
let _contentTableList = [];
const rowRegex = /]*>([\s\S]*?)<\/tr>/g; // 匹配表格行,[\s\S] 匹配所有字符
const cellRegex = /<(th|td)[^>]*>([\s\S]*?)<\/\1>/g; // 匹配单元格,[\s\S] 匹配所有字符
let rowMatch;
while ((rowMatch = rowRegex.exec(_contentHtml)) !== null) {
const rowContent = rowMatch[1]; // 每一行的内容
const rowData = [];
let cellMatch;
// 匹配每个单元格 (th 或 td)
while ((cellMatch = cellRegex.exec(rowContent)) !== null) {
let _text = cellMatch[2].trim();
_text = _text.replace(/&[a-zA-Z]+;/g, "");
_text = _text.replace(/<\/?[^>]+(>|$)/g, "");
rowData.push(_text); // 将每个单元格的内容添加到当前行的数组中
}
// 如果该行有数据,推送到 _contentTableList 中
if (rowData.length) {
_contentTableList.push(rowData);
}
}
// 输出提取的表格数据
resolve(_contentTableList);
});
},
getEChartsDataMixin(type = "1", content) {
return new Promise((resolve) => {
if (type === "1") {
//词云图
return this.getContentTableMixin(content).then(res => {
try {
if (res.length <= 0) {
return resolve({ data: 0 })//未找到表格数据
}
let _result = [];
res.forEach((item, index) => {
if (index == 0) return; //去掉表头
let _valueItem = item[2] ? item[2] : item[1];
let _value = _valueItem.match(/(\d+)/);
_value = _value ? parseInt(_value[0]) : 0;
_result.push({
value: _value,
name: item[0],
textStyle: { color: this.getRandomColorMixin() }
});
});
let _option = {
tooltip: {
show: false
},
series: [
{
type: "wordCloud",
sizeRange: [14, 38],
rotationRange: [0, 0],
keepAspect: false,
shape: "circle",
left: "center",
top: "center",
right: null,
bottom: null,
width: "100%",
height: "100%",
// rotationRange: [-90, 90],
rotationStep: 20,
data: _result
}
]
};
resolve({ data: _option })
} catch (e) {
resolve({ data: 1, err: e })
}
});
} else if (type == "2") {
//雷达图
//雷达图
return this.getContentTableMixin(content).then(res => {
try {
if (res.length <= 0) {
return resolve({ data: 0 })//未找到表格数据
}
let radarData = [];
let seriesData = { value: [] };
res.forEach((item, index) => {
if (index == 0) return; //去掉表头
radarData.push({ name: item[0], max: 5 });
let _valueItem = item[1] ? item[1] : "0";
let _value = _valueItem.match(/(\d+)/);
_value = _value ? parseInt(_value[0]) : 0;
seriesData.value.push(_value);
});
let _option = {
legend: {
textStyle: {
color: '#000'
}
},
radar: {
// shape: 'circle',
indicator: radarData,
name: {
textStyle: {
color: '#000'
}
}
},
series: [
{
type: "radar",
data: [seriesData],
label: {
color: '#000'
}
}
]
};
resolve({ data: _option })
} catch (e) {
resolve({ data: 1, err: e })
}
});
} else if (type == "3") {
//能量柱图
return this.getContentTableMixin(content).then(res => {
try {
if (res.length <= 0) {
return resolve({ data: 0 })//未找到表格数据
}
let _data = [];
let stepList = [];
stepList = this.calculateTopValuesMixin(res.length - 1);
res.forEach((item, index) => {
if (index == 0) return;
let _valueItem = item[1] ? item[1] : "0";
let _value = _valueItem.match(/(\d+)/);
_value = _value ? parseInt(_value[0]) : 0;
// 求百分比
_value = Math.floor((_value / 5).toFixed(2) * 100);
_data.push({
value: _value,
name: item[0],
title: { offsetCenter: ["0%", `${stepList[index - 1]}%`] },
detail: {
valueAnimation: true,
offsetCenter: ["0%", `${stepList[index - 1] + 15}%`]
}
});
});
let _option = {
series: [
{
type: "gauge",
startAngle: 90,
endAngle: -270,
pointer: {
show: false
},
progress: {
show: true,
overlap: false,
roundCap: true,
clip: false,
itemStyle: {
borderWidth: 1,
borderColor: "#464646"
}
},
axisLine: {
lineStyle: {
width: 40
}
},
splitLine: {
show: false,
distance: 0,
length: 10
},
axisTick: {
show: false
},
axisLabel: {
show: false,
distance: 50
},
data: _data,
title: {
fontSize: 14,
color: "#000"
},
detail: {
width: 50,
height: 14,
fontSize: 14,
color: "#000",
borderColor: "inherit",
borderRadius: 20,
borderWidth: 1,
formatter: "{value}%"
}
}
]
};
resolve({ data: _option })
} catch (e) {
resolve({ data: 1, err: e })
}
});
}
})
},
getRandomColorMixin() {
// 生成一个随机的 0 到 255 的数字,并将其转换为两位的十六进制形式
const randomHex = () => {
const hex = Math.floor(Math.random() * 256).toString(16);
return hex.length === 1 ? "0" + hex : hex; // 确保每个部分有两位
};
// 组合三种颜色(红、绿、蓝)的随机值
return `#${randomHex()}${randomHex()}${randomHex()}`;
},
calculateTopValuesMixin(len, minTop = -80, maxTop = 70, maxStep = 40) {
const length = len;
const middleIndex = Math.floor(length / 2); // 中间位置的索引
const totalRange = maxTop - minTop;
let step = totalRange / (length - 1); // 默认间隔
// 如果间隔大于 10%,则设置为最大 10%
if (step > maxStep) {
step = maxStep;
}
const topValues = [];
// 计算中间元素的top值
const middleTop = (minTop + maxTop) / 2;
topValues[middleIndex] = middleTop;
// 从中间向两边扩展,确保每个元素的top值
for (let i = middleIndex - 1; i >= 0; i--) {
topValues[i] = topValues[i + 1] - step; // 向上扩展
}
for (let i = middleIndex + 1; i < length; i++) {
topValues[i] = topValues[i - 1] + step; // 向下扩展
}
return topValues;
},
// 课堂时间分配
getTimeAllocationDataMixin(_dataList, fileId) {
return new Promise(async (resolve) => {
let _data = _dataList.reduce(
(pre, cur) => {
if (cur.role == "学生") {
pre[1].value += this.convertToSeconds(cur.time);
} else if (cur.role == "老师") {
pre[0].value += this.convertToSeconds(cur.time);
}
return pre;
},
[
{ value: 0, name: "老师" },
{ value: 0, name: "学生" }
]
);
let _dataPercentage = JSON.parse(JSON.stringify(_data));
_data.forEach((i, index) => {
_dataPercentage[index].percentage =
(
(i.value / _data.reduce((pre, cur) => pre + cur.value, 0)) *
100
).toFixed(2) + "%";
});
const _option = {
tooltip: {
left: "center",
trigger: "item",
formatter: "{a}
{b}: {d}%",
textStyle: {
color: '#000000'
}
},
legend: {
top: "5%",
left: "center",
textStyle: {
color: '#000000'
}
},
series: [
{
name: "课堂时间分配",
type: "pie",
radius: ["40%", "70%"],
label: {
formatter: "{b}: {d}%",
color: '#000000'
},
emphasis: {
label: {
show: true,
formatter: "{b}: {d}%",
color: '#000000'
},
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: "rgba(0, 0, 0, 0.5)"
}
},
data: _data
}
]
};
let _msg = `这是某一节课的师生时间占比,请你分析,写出结论,并给出指导建议。请使用3句完整的话,分析并给出建议。 请注意,当老师或学生的时间占比在【40~59%】之间的时候,也认为师生占比约为1:1,各占50%,师生时间占比比较均衡。
师生时间占比数据:
老师占比:${_dataPercentage[0].percentage}
学生占比:${_dataPercentage[1].percentage}
`;
let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
if (message.data == 1) {
return resolve({ eCharts: _option, message: "" })
} else {
return resolve({ eCharts: _option, message: message.data })
}
})
// return this.getAiContent(_msg);
},
// 师生互动分析
getInteractionAnalysisData(_dataList, fileId) {
return new Promise(async (resolve) => {
let _pushData = [0, 0];
let _result = [];
_dataList.forEach(i => {
if (i.role == "老师") {
_pushData[0] += this.convertToSeconds(i.time);
} else if (i.role == "学生") {
_pushData[1] += this.convertToSeconds(i.time);
}
return _result.push(JSON.parse(JSON.stringify(_pushData)));
});
let _flatArray = _result.flat();
const _max = Math.max(..._flatArray);
const _maxValue = Math.ceil(_max / 100) * 100;
const _option = {
xAxis: {
name: "老师", // X轴标题
nameLocation: "end", // 标题位置
scale: true,
min: 0,
max: _maxValue,
axisLabel: {
color: "#000" // 设置字体颜色为#000
},
nameTextStyle: {
color: "#000" // 设置老师字体颜色为#000
}
},
yAxis: {
name: "学生", // Y轴标题
nameLocation: "end", // 标题位置
scale: true,
min: 0,
max: _maxValue,
axisLabel: {
color: "#000" // 设置字体颜色为#000
},
nameTextStyle: {
color: "#000" // 设置学生字体颜色为#000
}
},
grid: {
containLabel: true
},
series: [
{
name: "数据",
step: "start",
data: _result,
type: "line",
lineStyle: {
}
},
{
name: "对角线",
type: "line",
data: [
[0, 0],
[_maxValue, _maxValue]
],
lineStyle: {
type: "dashed",
},
markLine: {
symbol: ["none", "none"]
}
}
]
};
let _msg = `
## 任务
请你结合 FIAS 相关的知识,根据以下提供给你的课堂原始数据(包含S和t的数据),请你具体描述整个课堂S行为与T行为的持续性与变化性。比如,课堂一开始老师占比主导,大约5分钟之后,进入到学生为主的小组讨论环节。在整个课堂之中,老师与学生的互动比较频繁,老师会频繁询问学生问题,引导学生思考。之后是授课时间与问答时间。等等。
## 输出要求 请使用自然语言进行描述,使用不超过5句完整的话进行整体性、概括性的描述,不要包含具体的时长信息。总结性概括之后,使用1句话对整个课堂的教师引导行为进行鼓励和评价,再使用1句话给出相应的优化建议。
## 你的知识库 定义与目的:S-T图,即学生-教师(Student-Teacher)图,主要用于记录和分析课堂上的学生行为(S)与教师行为(T)的时间分布。这种图形能够帮助教育专家和教师可视化课堂互动的流程,从而判断课堂的教学型态,如练习型、对话型、讲授型或混合型。 绘制方法:S-T图的绘制开始于教学的起始时刻,纵轴表示学生行为(S),横轴表示教师行为(T)。实际课堂观察或录像回放中,按照固定时间间隔(通常每30秒)采样,将对应的行为按时间顺序标记在相应的轴上。通过这种方法,可以清晰看到课堂上教师行为与学生行为的交替模式及其随时间的变化。 应用场景:例如,一个典型的应用是在分析不同类型课堂活动时使用S-T图。在讲授型课堂中,教师行为的时间占比会较高,S-T图显示较长的横轴(T行为)延续;而在练习型或对话型课堂中,学生行为的时间占比增高,显示为较长的纵轴(S行为)。
## 课堂实录
${JSON.stringify(_dataList)}
`;
let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
if (message.data == 1) {
return resolve({ eCharts: _option, message: "" })
} else {
return resolve({ eCharts: _option, message: message.data })
}
})
},
// 教学模式分析
getTeachingModeData(_dataList, fileId) {
return new Promise(async (resolve) => {
let _continuousTime = 0;
let _totalTime = 0;
let _continuousRole = "老师";
let _teacherTime = 0;
_dataList.forEach((item, index) => {
if (index == 0) {
//第一个
_continuousRole = item.role;
} else if (_dataList.length - 1 == index) {
//最后一个
if (_continuousRole == item.role) {
//连续对话了
_continuousTime += this.convertToSeconds(_dataList[index - 1].time);
_continuousTime += this.convertToSeconds(item.time);
} else {
//没连续对话
if (index >= 2) {
if (_dataList[index - 2].role == _dataList[index - 1].role) {
_continuousTime += this.convertToSeconds(
_dataList[index - 1].time
);
} else {
_continuousRole = item.role;
}
} else {
_continuousRole = item.role;
}
}
} else {
if (_continuousRole == item.role) {
//连续对话了
_continuousTime += this.convertToSeconds(_dataList[index - 1].time);
} else {
//没连续对话
if (index >= 2) {
if (_dataList[index - 2].role == _dataList[index - 1].role) {
_continuousTime += this.convertToSeconds(
_dataList[index - 1].time
);
} else {
_continuousRole = item.role;
}
} else {
_continuousRole = item.role;
}
}
}
if (item.role == "老师") {
_teacherTime += this.convertToSeconds(item.time);
}
_totalTime += this.convertToSeconds(item.time);
});
let _RT = (_teacherTime / _totalTime).toFixed(2);
let _CH = (_continuousTime / _totalTime).toFixed(2);
let _msg = `## 任务
根据FIAS(弗兰德斯互动分析系统)理论,计算获得某一节课的RT和CH值。请你结合FIAS相关知识进行分析,使用3句完整的话对整个课堂进行分析,需注意包含这些内容:分析该课堂所属的教学模型,描述课堂的整体表现与特征,肯定老师做出的努力,以及给出相应的建议。
## 你的知识
根据RT和CH的值,教学模式通常被分为以下几种类型: 练习型:RT ≤ 0.3,表示学生行为占主导,教师行为较少。 讲授型:RT ≥ 0.7,表示教师行为占主导,学生参与较少。 对话型:CH ≥ 0.4,表示师生之间有较多的互动和转换。 混合型:0.3 < RT < 0.7,CH < 0.4,表示教学中既有教师讲授也有学生参与,但两者都不占绝对优势。
## 数据
RT:${_RT}
CH:${_CH}
`;
let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
if (message.data == 1) {
return resolve({ RT: _RT, CH: _CH, message: "" })
} else {
return resolve({ RT: _RT, CH: _CH, message: message.data })
}
})
},
//光谱图
getSpectrogram(_dataList, fileId, content, assistant) {
return new Promise(async (resolve) => {
try {
this.getContentTableMixin(content).then(async res => {
if (res.length <= 0) {
resolve({ data: 1, err: "无表格数据" })
}
let _tableData = res;
let _delIndex = _tableData.findIndex(i => i.includes("时间点"))
_tableData = _tableData.slice(_delIndex + 1)
let _result = [];
let identity = "老师"; //0:老师 1:学生
let startTime = "";
let endTime = "";
let sumTime = 0;
let upTime = '00:00:00';
_dataList.forEach((item, index) => {
if (index == 0) {
//第一个
identity = item.role;
startTime = item.startTime;
endTime = item.endTime;
sumTime = (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
upTime = item.endTime
// console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
return;
}
if (item.role == identity) {
//没更换角色
sumTime += (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
endTime = item.endTime;
upTime = item.endTime
// console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
} else {
//更换角色了
_result.push({
startTime: startTime,
endTime: endTime,
identity: identity,
sumTime: sumTime
});
identity = item.role;
startTime = item.startTime;
endTime = item.endTime;
sumTime = (this.convertToSeconds(item.endTime) - this.convertToSeconds(upTime));
upTime = item.endTime
// console.log(item.endTime,item.startTime,(this.convertToSeconds(item.endTime) - this.convertToSeconds(item.startTime)))
}
if (index == _dataList.length - 1) {
// console.log("👉???",this.convertToSeconds(item.endTime))
}
});
let breakpoint = [];
breakpoint = _tableData.map(i => this.convertToSeconds(i[0]))
_result = _result.filter(
i =>
i.identity == "老师" ||
i.identity == "学生"
);
// let
let _data = {
data: [],
breakpoint: []
};
_data.data = _result.map(i => ({ role: i.identity, type: i.identity == "老师" ? 0 : 1, value: i.sumTime }));
_data.breakpoint = breakpoint;
let _msg = assistant.tips;
let message = await this.getAiContentMixin({ _msg: _msg, fileId: fileId })
if (message.data == 1) {
return resolve({ eCharts: _data, message: "" })
} else {
return resolve({ eCharts: _data, message: message.data })
}
// spectrogramData _data
});
} catch (e) {
return resolve({ data: 1, err: e })
}
})
},
getAiContentMixin(obj) {
return new Promise((resolve) => {
let { _msg, fileId } = obj
let parm = {
id: "f8e1ebb2-2e0d-11ef-8bf4-12e77c4cb76b",
message: _msg,
session_name: uuidv4(),
userId: this.userId,
file_ids: fileId ? [fileId] : [],
model: "gpt-4o-2024-11-20",
sound_url: "",
temperature: 0.2,
top_p: 1,
max_completion_tokens: 4096,
stream: false,
uid: uuidv4()
};
this.ajax
.post("https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat", parm)
.then(res => {
let _data = res.data;
resolve({ data: _data.message })
})
.catch(err => {
resolve({ data: 1, err: err })
});
})
},
convertToSeconds(time) {
let parts = time.split(":");
let seconds = +parts[0] * 3600 + +parts[1] * 60 + +parts[2];
return seconds;
},
//创建课堂
createClassMixin(data) {
return new Promise((resolve) => {
let _analysisList = data.analysisList;
let createJson = _analysisList.map(i => {
return {
jsonData: i.jsonData,
type: i.Type,
index: i.tIndex
}
})
createJson = createJson.filter(i => !i.isOtherData && converter(i.jsonData.name) != converter("词频词汇分析"));
let params = {
tid: uuidv4(),
userid: this.userId,
template: createJson
}
this.ajax
.post(
"https://gpt4.cocorobo.cn/insert_classroom_observation_template",
params
)
.then(res => {
let _data = res.data.FunctionResponse;
if (converter(_data.message) == converter("创建成功")) {
this.ajax
.post("https://gpt4.cocorobo.cn/insert_classroom_observation", {
tid: params.tid,
type: 10,
index: 0,
json_data: JSON.stringify({ file_ids: data.file_ids }),
userid: this.userId
})
.then(res2 => {
let _data2 = res2.data.FunctionResponse;
if (converter(_data2.message) == converter("创建成功")) {
let newOption = { id: uuidv4(), label: data.baseMessage.courseName, value: params.tid }
let params2 = {
tid: params.tid,
type: 0,
}
this.ajax
.post(
"https://gpt4.cocorobo.cn/get_classroom_observation_new",
params2
).then(res3 => {
let _data = res3.data.FunctionResponse.result.length
? JSON.parse(res3.data.FunctionResponse.result)
: [];
//替换基础信息
let _bmData = _data.find(i => i.tIndex == 0);
let _imageList = _data.find(i => i.tIndex == 1);
_imageList.jsonData = data.baseMessage.imageList
if (_bmData) {
_bmData.jsonData = data.baseMessage;
delete _bmData.jsonData.imageList;
if (data.tagList) {
_bmData.jsonData.dialogTagList = data.tagList;
} else {
_bmData.jsonData.dialogTagList = [
{ value: 0, name: "通用课堂分析", loading: false },
{ value: 1, name: "学科课堂分析", loading: false },
{ value: 2, name: "扩展分析", loading: false }
]
}
}
let arr = [{ id: _bmData.id, jsonData: JSON.stringify(_bmData.jsonData) }, { id: _imageList.id, jsonData: JSON.stringify(_imageList.jsonData) }]
let promises = [];
arr.forEach(i => {
promises.push(new Promise((resolve) => {
this.ajax
.post("https://gpt4.cocorobo.cn/update_classroom_observation", {
id: i.id,
json_data: i.jsonData
})
.then(res => {
resolve();
})
.catch(e => {
console.log("保存失败", e);
resolve()
});
}))
})
Promise.all(promises).then(res => {
resolve({ data: newOption, tid: params.tid })
})
}).catch(err => {
console.log("修改基础信息失败", err)
resolve({ data: 3, err: err })
})
}
}).catch(err => {
resolve({ data: 2, err: err })
console.log("存储fileId失败")
console.log(err)
})
}
}).catch(err => {
resolve({ data: 1, err: err })
console.log("创建课堂失败")
console.log(err)
})
})
},
//文本转录
wavAudioToTextAndObjMixin(file) {
return new Promise(async (resolve) => {
let iframeRef = this.$refs["iframeRef"];
iframeRef.contentWindow.window.document.getElementById(
"languageOptions"
).selectedIndex = 2;//默认普通话
let transcriptionContent = "";
let tableContent = "";
let tableList = [];
let _startTime = 0;
let _endTime = 0;
// 转录中
iframeRef.contentWindow.onRecognizedResult = (e) => {
let privText = e.privText;
let privSpeakerId = e.privSpeakerId;
let privDuration = e.privDuration;
let privOffset = e.privOffset;
if (!privText || !privSpeakerId || privSpeakerId == "Unknown") {//不记录
return;
}
_endTime = (privOffset + privDuration) / 10000000;
tableList.push({
value: privText,
startTime: this.updateRecordedTimeMixin({ duration: _startTime }),
endTime: this.updateRecordedTimeMixin({ duration: _endTime }),
time: this.updateRecordedTimeMixin({ duration: _endTime - _startTime }),
role: privSpeakerId,
code: ""
});
_startTime = _endTime;
transcriptionContent += privText;
};
//转录结束
iframeRef.contentWindow.onSessionStopped = async (e) => {
tableContent = `
序号 |
开始时间 |
结束时间 |
发言内容 |
时长 |
说话人身份 |
行为编码 |
`;
tableList.forEach((item, index) => {
tableContent += `
${index + 1} |
${item.startTime} |
${item.endTime} |
${item.value} |
${item.time} |
${item.role} |
${item.code} |
`
})
tableContent += ` | | | | | | |
`
var blob = new Blob([tableContent], { type: "text/plain;charset=utf-8" });
blob.lastModifiedDate = new Date();
blob.name = `classroomObservation.txt`;
this.uploadFileMixin(blob).then(upload => {
resolve({ transcriptionContent: transcriptionContent, editorBarData: { type: "0", url: upload.Location, content: tableContent, tableList: tableList } })
})
};
//开始转录
iframeRef.contentWindow.ConversationTranscriber({
files: [file]
});
})
},
updateRecordedTimeMixin({ duration }) {
// 更新currentTime,将秒数转换为时分秒格式
let hours = Math.floor(duration / 3600);
let minutes = Math.floor((duration % 3600) / 60);
let seconds = Math.floor(duration % 60);
// this.recordedForm.time = `${hours.toString().padStart(2, "0")}:${minutes
// .toString()
// .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
return `${hours
.toString()
.padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
},
//自动编码
automaticCodingMixin(data) {
return new Promise(async (resolve) => {
let { tableList } = data;
let roleObj = {};
let tableContent = `
序号 |
开始时间 |
结束时间 |
发言内容 |
时长 |
说话人身份 |
行为编码 |
`;
console.log("说话人身份编码开始")
// 说话人身份编码
while (tableList.some(i => i.role.indexOf("Guest") != -1 && i.role !== '')) {
let _ajaxList = tableList.filter(i => i.role.indexOf("Guest") != -1 && i.role !== '').slice(0, 10);
console.log(`说话人身份编码:`, _ajaxList)
const params = {
inputs: {
options: "老师,学生",
rows: JSON.stringify(
_ajaxList.map(i => {
return { content: i.value, role: i.role };
})
)
},
response_mode: "blocking",
user: this.userId
};
let roleRes = await this.getWavRoleList(params);
if (roleRes.data === 1) continue;;
let _roleResult = roleRes.data.data.outputs.result;
let _numRole = [];
_roleResult.forEach((txt, index) => {
let _oldRole = _ajaxList[index].role;
if (_numRole.map(i => i.role).includes(_oldRole)) {
let _findIndex = _numRole.findIndex(
i => i.role == _oldRole
);
if (txt == "学生") {
_numRole[_findIndex].s += 1;
} else if (txt == "老师") {
_numRole[_findIndex].t += 1;
}
} else {
if (txt == "学生") {
_numRole.push({ role: _oldRole, t: 0, s: 1 });
} else if (txt == "老师") {
_numRole.push({ role: _oldRole, t: 1, s: 0 });
}
}
});
//根据数量判断是老师还是学生
_numRole.forEach(i => {
if (i.t > i.s) {
roleObj[i.role] = "老师";
} else if (i.t < i.s) {
roleObj[i.role] = "学生";
}
});
//已经有的role
let roleKeys = Object.keys(roleObj);
tableList.forEach(i => {
if (roleKeys.includes(i.role)) {
i.role = roleObj[i.role];
}
});
}
console.log("说话人身份编码完成")
console.log("说话人行为编码开始")
//说话人行为编码
while (tableList.some(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "")) {
let _ajaxList = tableList.filter(i => i.code == "" && i.role.indexOf("Guest") == -1 && i.value != "").slice(0, 10);
console.log(`说话人行为编码:`, _ajaxList)
let params = {
inputs: {
rows: JSON.stringify(
_ajaxList.map(i => ({
content: i.value,
role: i.role
}))
),
options: "老师讲课,老师提问或点名,老师板书或操作,老师评价或反馈,老师其他,学生发言,学生小组活动,学生自主学习,学生汇报分享,学生其他",
attention: "- 先根据说话人角色判断,再在对应角色的选项中选择选项\n- 如果没有合适的选项,默认使用`老师其他`或者`学生其他`"
},
response_mode: "blocking",
user: this.userId
};
let _codeRes = await this.getBehavioralCoding(params)
if (_codeRes.data === 1) continue;
const _codeResult = _codeRes.data.data.outputs.result;
_ajaxList.forEach((item, index) => {
let _findIndex = tableList.findIndex(i => i.index === item.index);
if (_findIndex != -1) {
tableList[_findIndex].code = _codeResult[index];
}
})
}
console.log("说话人行为编码完成")
tableList.forEach((item, index) => {
tableContent += `
${index + 1} |
${item.startTime} |
${item.endTime} |
${item.value} |
${item.time} |
${item.role} |
${item.code} |
`
})
tableContent += ` | | | | | | |
`
var blob = new Blob([tableContent], { type: "text/plain;charset=utf-8" });
blob.lastModifiedDate = new Date();
blob.name = `classroomObservation.txt`;
this.uploadFileMixin(blob).then(upload => {
resolve({ editorBarData: { type: "0", url: upload.Location, content: tableContent, tableList: tableList } })
})
})
},
getWavRoleList(params) {
return new Promise((resolve, reject) => {
this.ajax
.post("https://dify.cocorobo.cn/v1/workflows/run?key=role", params)
.then(res => {
resolve(res);
})
.catch(err => {
console.log("获取说话人身份失败", err)
resolve({ data: 1 })
// reject(err);
});
});
},
getBehavioralCoding(params) {
return new Promise((resolve, reject) => {
this.ajax
.post("https://dify.cocorobo.cn/v1/workflows/run?key=code", params)
.then(res => {
resolve(res);
})
.catch(err => {
console.log("获取说话人编码失败", err)
resolve({ data: 1 })
});
});
},
//m4a转wav
audioToWavMixin(fileObj) {
return new Promise((resolve) => {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const reader = new FileReader();
reader.onload = (e)=>{
const arrayBuffer = e.target.result;
// 解码音频数据
audioContext.decodeAudioData(arrayBuffer)
.then(audioBuffer => {
let wavBlob = this.bufferToWav(audioBuffer);
let _wavFile = new File([wavBlob], "audio.wav", {
type: "audio/wav"
})
// 在控制台输出WAV文件对象
resolve({ data: _wavFile })
console.log('转换后的WAV文件对象:', _wavFile);
})
.catch(err => {
resolve({ data: 1, err: err })
});
};
reader.onerror = (e) => {
resolve({ data: 1, err: e })
};
reader.readAsArrayBuffer(fileObj);
})
}
}
};