mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 16:19:18 +00:00
✨ feat: 新增哔哩哔哩官方AI总结、哔哩哔哩音乐提取
1. 新增哔哩哔哩音乐提取,使用“bili音乐+链接”即可提取视频中的音乐 2. 重构部分代码 3. 更换tiktok API 4. 更换视频总结方式GPT为官方的视频摘要(免费使用) 5. 删除GPT相关内容
This commit is contained in:
parent
444830003c
commit
d0e6e6e5bd
38
README.md
38
README.md
@ -61,7 +61,7 @@ sudo apt-get install ffmpeg
|
||||
|
||||
<img src="./img/example6.webp" alt="小程序解析" width="50%" height="50%" />
|
||||
|
||||
5. 【可选】对哔哩哔哩解析进行总结:需要填写accessToken和哔哩哔哩的SESSDATA
|
||||
5. 【可选】对哔哩哔哩解析进行总结:需要填写哔哩哔哩的SESSDATA
|
||||
|
||||
<img src="./img/example7.webp" alt="小程序解析" width="50%" height="50%" />
|
||||
|
||||
@ -70,25 +70,12 @@ sudo apt-get install ffmpeg
|
||||
- 锅巴设置
|
||||
|
||||
## 🐤 Q&A
|
||||
### bilibili问题
|
||||
> 哔哩哔哩的SESSDATA?
|
||||
> 进入哔哩哔哩网站 -- 打开F12开发者选项 -- 应用 -- 找到Cookie -- 找到SESSDATA -- 复制 -- 粘贴到plugins/rconsole-plugin/config/tools.yaml or 锅巴
|
||||
> [图文教程【群友推荐!】](https://www.bilibili.com/read/cv12349604)
|
||||
|
||||
## 🤺 R插件交流群
|
||||
扫码不行就:575663150
|
||||
|
||||
<img src="./img/qq.jpg" width="30%" height="30%">
|
||||
|
||||
## 🧑🌾 进阶内容
|
||||
【可选】相关配置(apps/tools.js):
|
||||
> `百度翻译`api:https://fanyi-api.baidu.com/doc/21
|
||||
注册完填入方式参考上方注释url (config/tools.yaml);另外,有群友反馈百度翻译需要充钱才能使用!
|
||||
|
||||
> (非必要不更改)更改魔法在`config/tools.yaml` 或 [锅巴插件](https://gitee.com/guoba-yunzai/guoba-plugin)的配置位置:
|
||||
`proxyAddr: '127.0.0.1' # 魔法地址`
|
||||
`proxyPort: '7890' # 魔法端口`
|
||||
|
||||
## 🎵 douyin_cookie问题
|
||||
### 🎵 douyin问题
|
||||
由于douyin的解析变化莫测,现版本需要填入自己的cookie,具体步骤如下:
|
||||
1. 打开`https://www.douyin.com/` 扫码登入自己的账号
|
||||
2. F12进入控制台,或者下载一个[Cookie-Editor](https://www.crxsoso.com/webstore/detail/hlkenndednhfkekhgcdicdfddnkalmdm)
|
||||
@ -105,6 +92,25 @@ sudo apt-get install ffmpeg
|
||||
- msToken
|
||||

|
||||
|
||||
## 🤺 R插件交流群
|
||||
扫码不行就:575663150
|
||||
|
||||
<img src="./img/qq.jpg" width="30%" height="30%">
|
||||
|
||||
## 🧑🌾 进阶内容
|
||||
【可选】相关配置(apps/tools.js):
|
||||
> `百度翻译`api:https://fanyi-api.baidu.com/doc/21
|
||||
注册完填入方式参考上方注释url (config/tools.yaml);另外,有群友反馈百度翻译需要充钱才能使用!
|
||||
|
||||
> (非必要不更改)更改魔法在`config/tools.yaml` 或 [锅巴插件](https://gitee.com/guoba-yunzai/guoba-plugin)的配置位置:
|
||||
`proxyAddr: '127.0.0.1' # 魔法地址`
|
||||
`proxyPort: '7890' # 魔法端口`
|
||||
|
||||
> 海外服务器示例:
|
||||
`proxyAddr: '127.0.0.1' # 魔法地址`
|
||||
`proxyPort: '80' # 魔法端口`
|
||||
|
||||
|
||||
## 📦 业务
|
||||

|
||||
|
||||
|
@ -6,7 +6,7 @@ import puppeteer from "../../../lib/puppeteer/puppeteer.js";
|
||||
// http库
|
||||
import axios from "axios";
|
||||
// 常量
|
||||
import { CAT_LIMIT } from "../utils/constant.js";
|
||||
import { CAT_LIMIT } from "../constants/constant.js";
|
||||
// 书库
|
||||
import { getZHelper, getYiBook, getZBook } from "../utils/books.js";
|
||||
// 工具类
|
||||
|
210
apps/tools.js
210
apps/tools.js
@ -7,10 +7,15 @@ import _ from "lodash";
|
||||
import tunnel from "tunnel";
|
||||
import HttpProxyAgent from "https-proxy-agent";
|
||||
import { mkdirIfNotExists, checkAndRemoveFile, deleteFolderRecursive } from "../utils/file.js";
|
||||
import { downloadBFile, getDownloadUrl, mergeFileToMp4 } from "../utils/bilibili.js";
|
||||
import { downloadBFile, getAudioUrl, getDownloadUrl, mergeFileToMp4 } from "../utils/bilibili.js";
|
||||
import { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 } from "../utils/acfun.js";
|
||||
import { transMap, douyinTypeMap, XHS_CK, TEN_THOUSAND } from "../utils/constant.js";
|
||||
import { getIdVideo } from "../utils/common.js";
|
||||
import {
|
||||
transMap,
|
||||
douyinTypeMap,
|
||||
XHS_CK,
|
||||
RESTRICTION_DESCRIPTION,
|
||||
} from "../constants/constant.js";
|
||||
import { dataProcessing, formatBiliInfo, getIdVideo, secondsToTime } from "../utils/common.js";
|
||||
import config from "../model/index.js";
|
||||
import Translate from "../utils/trans-strategy.js";
|
||||
import * as xBogus from "../utils/x-bogus.cjs";
|
||||
@ -21,6 +26,8 @@ import { ChatGPTBrowserClient, ChatGPTClient } from "@waylaidwanderer/chatgpt-ap
|
||||
import { av2BV } from "../utils/bilibili-bv-av-convert.js";
|
||||
import querystring from "querystring";
|
||||
import TokenBucket from "../utils/token-bucket.js";
|
||||
import { getWbi } from "../utils/biliWbi.js";
|
||||
import { BILI_SUMMARY } from "../constants/bili.js";
|
||||
|
||||
export class tools extends plugin {
|
||||
constructor() {
|
||||
@ -99,23 +106,6 @@ export class tools extends plugin {
|
||||
this.biliDuration = this.toolsConfig.biliDuration;
|
||||
// 加载抖音Cookie
|
||||
this.douyinCookie = this.toolsConfig.douyinCookie;
|
||||
// 加载gpt配置:accessToken、apiKey、模型
|
||||
this.openaiAccessToken = this.toolsConfig.openaiAccessToken;
|
||||
this.openaiApiKey = this.toolsConfig.openaiApiKey;
|
||||
this.openaiModel = this.toolsConfig.openaiModel;
|
||||
// 加载gpt客户端(默认加载sk,如果填了AccessToken就用AccessToken)
|
||||
this.chatGptClient = this.openaiAccessToken === '' ? new ChatGPTClient(this.openaiApiKey, {
|
||||
modelOptions: {
|
||||
model: this.openaiModel,
|
||||
temperature: 0,
|
||||
},
|
||||
proxy: this.myProxy,
|
||||
debug: false,
|
||||
}) : new ChatGPTBrowserClient({
|
||||
reverseProxyUrl: "https://bypass.churchless.tech/api/conversation",
|
||||
accessToken: this.openaiAccessToken,
|
||||
model: this.openaiModel,
|
||||
})
|
||||
}
|
||||
|
||||
// 翻译插件
|
||||
@ -182,7 +172,7 @@ export class tools extends plugin {
|
||||
e.reply("解析失败,请重试!");
|
||||
return;
|
||||
}
|
||||
console.log(resp.data)
|
||||
// console.log(resp.data)
|
||||
const item = resp.data.aweme_detail;
|
||||
e.reply(`识别:抖音, ${ item.desc }`);
|
||||
const urlTypeCode = item.aweme_type;
|
||||
@ -253,10 +243,10 @@ export class tools extends plugin {
|
||||
} else {
|
||||
url = urlRex.exec(url)[0];
|
||||
}
|
||||
let idVideo = await getIdVideo(url);
|
||||
idVideo = idVideo.replace(/\//g, "");
|
||||
let tiktokVideoId = await getIdVideo(url);
|
||||
tiktokVideoId = tiktokVideoId.replace(/\//g, "");
|
||||
// API链接
|
||||
const API_URL = `https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/feed/?aweme_id=${idVideo}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9`;
|
||||
const API_URL = `https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/feed/?aweme_id=${ tiktokVideoId }`;
|
||||
|
||||
await axios
|
||||
.get(API_URL, {
|
||||
@ -295,6 +285,7 @@ export class tools extends plugin {
|
||||
this.biliCore(e);
|
||||
});
|
||||
}
|
||||
|
||||
async biliCore(e) {
|
||||
const urlRex = /(?:https?:\/\/)?www\.bilibili\.com\/[A-Za-z\d._?%&+\-=\/#]*/g;
|
||||
const bShortRex = /(http:|https:)\/\/b23.tv\/[A-Za-z\d._?%&+\-=\/#]*/g;
|
||||
@ -317,69 +308,56 @@ export class tools extends plugin {
|
||||
if (matched) {
|
||||
url = url.replace(matched[0], av2BV(Number(matched[2])));
|
||||
}
|
||||
// 动态
|
||||
// 动态处理
|
||||
if (url.includes("t.bilibili.com")) {
|
||||
// 去除多余参数
|
||||
if (url.includes("?")) {
|
||||
url = url.substring(0, url.indexOf("?"));
|
||||
}
|
||||
const dynamicId = /[^/]+(?!.*\/)/.exec(url)[0];
|
||||
getDynamic(dynamicId).then(async resp => {
|
||||
if (resp.dynamicSrc.length > 0) {
|
||||
e.reply(`识别:哔哩哔哩动态, ${resp.dynamicDesc}`);
|
||||
let dynamicSrcMsg = [];
|
||||
resp.dynamicSrc.forEach(item => {
|
||||
dynamicSrcMsg.push({
|
||||
message: segment.image(item),
|
||||
nickname: e.sender.card || e.user_id,
|
||||
user_id: e.user_id,
|
||||
});
|
||||
});
|
||||
await this.reply(await Bot.makeForwardMsg(dynamicSrcMsg));
|
||||
} else {
|
||||
e.reply(`识别:哔哩哔哩动态, 但是失败!`);
|
||||
}
|
||||
});
|
||||
url = this.biliDynamic(url, e);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 视频信息获取例子:http://api.bilibili.com/x/web-interface/view?bvid=BV1hY411m7cB
|
||||
// 请求视频信息
|
||||
const videoInfo = await getVideoInfo(url);
|
||||
const { title, pic, desc, duration, dynamic, stat, aid, cid, pages } = videoInfo;
|
||||
const { title, pic, desc, duration, dynamic, stat, bvid, aid, cid, owner, pages } = videoInfo;
|
||||
// 视频信息
|
||||
let { view, danmaku, reply, favorite, coin, share, like } = stat;
|
||||
// 数据处理
|
||||
const dataProcessing = data => {
|
||||
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + "万" : data;
|
||||
};
|
||||
// 限制时长 & 考虑分页视频情况
|
||||
const query = querystring.parse(url);
|
||||
const curPage = query?.p || 0;
|
||||
const curDuration = pages?.[curPage]?.duration || duration;
|
||||
const isLimitDuration = curDuration > this.biliDuration
|
||||
// 构造一个可扩展的Map
|
||||
const dataProcessMap = {
|
||||
"点赞": like,
|
||||
"硬币": coin,
|
||||
"收藏": favorite,
|
||||
"分享": share,
|
||||
"总播放量": view,
|
||||
"弹幕数量": danmaku,
|
||||
"评论": reply
|
||||
};
|
||||
// 格式化数据
|
||||
const combineContent =
|
||||
`\n点赞:${dataProcessing(like)} | 硬币:${dataProcessing(
|
||||
coin,
|
||||
)} | 收藏:${dataProcessing(favorite)} | 分享:${dataProcessing(share)}\n` +
|
||||
`总播放量:${dataProcessing(view)} | 弹幕数量:${dataProcessing(
|
||||
danmaku,
|
||||
)} | 评论:${dataProcessing(reply)}\n` +
|
||||
`简介:${desc}`;
|
||||
const combineContent = `\n${ formatBiliInfo(dataProcessMap) }\n简介:${ desc }`;
|
||||
let biliInfo = [`识别:哔哩哔哩:${ title }`, combineContent]
|
||||
// 只提取音乐处理
|
||||
if (e.msg.includes("bili音乐")) {
|
||||
return await this.biliMusic(url, e, biliInfo);
|
||||
}
|
||||
// 不提取音乐,正常处理
|
||||
if (isLimitDuration) {
|
||||
// 加入图片
|
||||
biliInfo.unshift(segment.image(pic))
|
||||
// 限制视频解析
|
||||
const durationInMinutes = (curDuration / 60).toFixed(0);
|
||||
biliInfo.push(`\n-----------------------限制说明-----------------------\n当前视频时长约:${durationInMinutes}分钟,\n大于管理员设置的最大时长 ${this.biliDuration / 60} 分钟!`)
|
||||
biliInfo.push(`${RESTRICTION_DESCRIPTION}\n当前视频时长约:${ durationInMinutes }分钟,\n大于管理员设置的最大时长 ${ this.biliDuration / 60 } 分钟!`)
|
||||
e.reply(biliInfo);
|
||||
// 总结
|
||||
const summary = await this.getBiliSummary(videoInfo);
|
||||
const summary = await this.getBiliSummary(bvid, cid, owner.mid);
|
||||
summary && e.reply(summary);
|
||||
return true;
|
||||
} else {
|
||||
// 总结
|
||||
const summary = await this.getBiliSummary(bvid, cid, owner.mid);
|
||||
summary && biliInfo.push(`\n${summary}`)
|
||||
//
|
||||
e.reply(biliInfo);
|
||||
}
|
||||
|
||||
@ -402,12 +380,90 @@ export class tools extends plugin {
|
||||
logger.error(err);
|
||||
e.reply("解析失败,请重试一下");
|
||||
});
|
||||
// 总结
|
||||
const summary = await this.getBiliSummary(videoInfo);
|
||||
summary && e.reply(summary);
|
||||
return true;
|
||||
}
|
||||
|
||||
async biliMusic(url, e, biliInfo) {
|
||||
const { audioUrl, title } = await getAudioUrl(url);
|
||||
e.reply(biliInfo)
|
||||
e.reply(segment.record(audioUrl))
|
||||
return true
|
||||
}
|
||||
|
||||
// 发送哔哩哔哩动态的算法
|
||||
biliDynamic(url, e) {
|
||||
// 去除多余参数
|
||||
if (url.includes("?")) {
|
||||
url = url.substring(0, url.indexOf("?"));
|
||||
}
|
||||
const dynamicId = /[^/]+(?!.*\/)/.exec(url)[0];
|
||||
getDynamic(dynamicId).then(async resp => {
|
||||
if (resp.dynamicSrc.length > 0) {
|
||||
e.reply(`识别:哔哩哔哩动态, ${ resp.dynamicDesc }`);
|
||||
let dynamicSrcMsg = [];
|
||||
resp.dynamicSrc.forEach(item => {
|
||||
dynamicSrcMsg.push({
|
||||
message: segment.image(item),
|
||||
nickname: e.sender.card || e.user_id,
|
||||
user_id: e.user_id,
|
||||
});
|
||||
});
|
||||
await this.reply(await Bot.makeForwardMsg(dynamicSrcMsg));
|
||||
} else {
|
||||
e.reply(`识别:哔哩哔哩动态, 但是失败!`);
|
||||
}
|
||||
});
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 哔哩哔哩总结
|
||||
* @author zhiyu1998
|
||||
* @param bvid 稿件
|
||||
* @param cid 视频 cid
|
||||
* @param up_mid UP主 mid
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async getBiliSummary(bvid, cid, up_mid) {
|
||||
// 这个有点用,但不多
|
||||
let wbi = "wts=1701546363&w_rid=1073871926b3ccd99bd790f0162af634"
|
||||
if (!_.isEmpty(this.biliSessData)) {
|
||||
wbi = await getWbi({ bvid, cid, up_mid }, this.biliSessData);
|
||||
}
|
||||
// 构造API
|
||||
const summaryUrl = `${ BILI_SUMMARY }?${ wbi }`;
|
||||
logger.info(summaryUrl)
|
||||
// 构造结果:https://api.bilibili.com/x/web-interface/view/conclusion/get?bvid=BV1L94y1H7CV&cid=1335073288&up_mid=297242063&wts=1701546363&w_rid=1073871926b3ccd99bd790f0162af634
|
||||
return axios.get(summaryUrl)
|
||||
.then(resp => {
|
||||
const data = resp.data.data?.model_result;
|
||||
// logger.info(data)
|
||||
const summary = data?.summary;
|
||||
const outline = data?.outline;
|
||||
let resReply = "";
|
||||
// 总体总结
|
||||
if (summary) {
|
||||
resReply = `摘要:${ summary }\n`
|
||||
}
|
||||
// 分段总结
|
||||
if (outline) {
|
||||
const specificTimeSummary = outline.map(item => {
|
||||
const smallTitle = item.title;
|
||||
const keyPoint = item?.part_outline;
|
||||
// 时间点的总结
|
||||
const specificContent = keyPoint.map(point => {
|
||||
const { timestamp, content } = point
|
||||
const specificTime = secondsToTime(timestamp)
|
||||
return `${ specificTime } ${ content }\n`;
|
||||
}).join("");
|
||||
return `- ${ smallTitle }\n${ specificContent }\n`;
|
||||
});
|
||||
resReply += specificTimeSummary.join("");
|
||||
}
|
||||
return resReply;
|
||||
})
|
||||
}
|
||||
|
||||
// 百科
|
||||
async wiki(e) {
|
||||
const key = e.msg.replace(/#|百科|wiki/g, "").trim();
|
||||
@ -678,7 +734,7 @@ export class tools extends plugin {
|
||||
suffix = suffix.replace("reel/", "p/");
|
||||
}
|
||||
const API = `https://imginn.com/${ suffix }`;
|
||||
logger.info(API);
|
||||
// logger.info(API);
|
||||
let imgPromise = [];
|
||||
const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`;
|
||||
// 简单封装图片下载
|
||||
@ -874,28 +930,6 @@ export class tools extends plugin {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 哔哩哔哩总结
|
||||
* @returns Promise{string}
|
||||
* @param videoInfo
|
||||
*/
|
||||
async getBiliSummary(videoInfo) {
|
||||
if (this.biliSessData && this.openaiAccessToken) {
|
||||
try {
|
||||
const prompt = await getBiliGptInputText(videoInfo, this.biliSessData);
|
||||
|
||||
const response = await this.chatGptClient.sendMessage(prompt);
|
||||
// 暂时不设计上下文
|
||||
return response.response
|
||||
} catch (err) {
|
||||
logger.error("总结失败,可能是没有弹幕或者网络问题!\n", err);
|
||||
return ""
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载一张网络图片(自动以url的最后一个为名字)
|
||||
* @param img
|
||||
|
@ -38,6 +38,9 @@
|
||||
- icon: bilibili
|
||||
title: "bilibili/b23"
|
||||
desc: 哔哩哔哩分享实时下载
|
||||
- icon: bilimusic
|
||||
title: "bili音乐+链接"
|
||||
desc: 哔哩哔哩音乐分享实时下载
|
||||
- icon: 推特
|
||||
title: "小蓝鸟"
|
||||
desc: 推特学习版分享实时下载
|
||||
|
@ -8,8 +8,4 @@ translateSecret: '' # 百度翻译密匙
|
||||
biliSessData: '' # 哔哩哔哩的SESSDATA
|
||||
biliDuration: 480 # 哔哩哔哩限制的最大视频时长(默认8分钟),单位:秒
|
||||
|
||||
openaiAccessToken: '' # 通过获取:https://chat.openai.com/api/auth/session
|
||||
openaiApiKey: '' # sk...
|
||||
openaiModel: 'gpt-3.5-turbo' # 目前gpt-3.5-turbo效果比较好,廉价,适合群友
|
||||
|
||||
douyinCookie: '' # douyin's cookie, 格式:odin_tt=xxx;sessionid_ss=xxx;ttwid=xxx;passport_csrf_token=xxx;msToken=xxx;
|
@ -1,11 +1,11 @@
|
||||
- {
|
||||
version: 1.1.3,
|
||||
version: 1.2.0,
|
||||
data:
|
||||
[
|
||||
新增<span class="cmd">哔哩哔哩官方AI总结</span>功能,
|
||||
新增<span class="cmd">哔哩哔哩音乐提取</span>功能,
|
||||
新增<span class="cmd">快手解析</span>功能,
|
||||
新增<span class="cmd">竹白百科</span>功能,
|
||||
重构<span class="cmd">翻译</span>功能,
|
||||
适配<span class="cmd">锅巴</span>插件,方便查看和修改配置,
|
||||
支持<span class="cmd">锅巴</span>插件,方便查看和修改配置,
|
||||
添加<span class="cmd">#R帮助</span>获取插件帮助,
|
||||
添加<span class="cmd">#R版本</span>获取插件版本,
|
||||
],
|
||||
|
@ -79,7 +79,7 @@ export function supportGuoba() {
|
||||
field: "tools.biliSessData",
|
||||
label: "哔哩哔哩SESSDATA",
|
||||
bottomHelpMessage:
|
||||
"如何获取具体参考我的文档说明:https://gitee.com/kyrzy0416/rconsole-plugin",
|
||||
"如何获取具体参考我的文档说明:https://gitee.com/kyrzy0416/rconsole-plugin#Q&A",
|
||||
component: "Input",
|
||||
required: false,
|
||||
componentProps: {
|
||||
@ -97,17 +97,6 @@ export function supportGuoba() {
|
||||
placeholder: "请输入哔哩哔哩的视频最大限制时长(默认15分钟)",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "tools.openaiAccessToken",
|
||||
label: "OpenAI的AccessToken",
|
||||
bottomHelpMessage:
|
||||
"ey....,先登录:https://chat.openai.com/,再复制里面的accessToken:https://chat.openai.com/api/auth/session",
|
||||
component: "Input",
|
||||
required: false,
|
||||
componentProps: {
|
||||
placeholder: "请输入OpenAI的AccessToken(ey.....)",
|
||||
},
|
||||
},
|
||||
{
|
||||
field: "tools.douyinCookie",
|
||||
label: "抖音的Cookie",
|
||||
|
@ -1,11 +1,12 @@
|
||||
import fetch from "node-fetch";
|
||||
import axios from "axios";
|
||||
import { BILI_VIDEO_INFO } from "../constants/bili.js";
|
||||
|
||||
async function getVideoInfo(url) {
|
||||
const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view";
|
||||
// const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view";
|
||||
const videoId = /video\/[^\?\/ ]+/.exec(url)[0].split("/")[1];
|
||||
// 获取视频信息,然后发送
|
||||
return fetch(`${baseVideoInfo}?bvid=${videoId}`)
|
||||
return fetch(`${BILI_VIDEO_INFO}?bvid=${videoId}`)
|
||||
.then(async resp => {
|
||||
const respJson = await resp.json();
|
||||
const respData = respJson.data;
|
||||
@ -16,8 +17,10 @@ async function getVideoInfo(url) {
|
||||
duration: respData.duration,
|
||||
dynamic: respJson.data.dynamic,
|
||||
stat: respData.stat,
|
||||
bvid: respData.bvid,
|
||||
aid: respData.aid,
|
||||
cid: respData.pages?.[0].cid,
|
||||
owner: respData.owner,
|
||||
pages: respData?.pages,
|
||||
};
|
||||
});
|
||||
|
@ -1,208 +0,0 @@
|
||||
/**
|
||||
* 获取gpt提取视频信息的文字
|
||||
* @param videoInfo
|
||||
* @param biliSessData
|
||||
* @param shouldShowTimestamp 是否在每段字幕前面加入时间标识
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getBiliGptInputText(videoInfo, biliSessData, shouldShowTimestamp = false) {
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
|
||||
Host: "api.bilibili.com",
|
||||
Cookie: `SESSDATA=${biliSessData}`,
|
||||
};
|
||||
const commonConfig = {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
headers,
|
||||
referrerPolicy: "no-referrer",
|
||||
};
|
||||
const { title, desc, dynamic, aid, cid } = videoInfo;
|
||||
// https://api.bilibili.com/x/player/v2?aid=438937138&cid=1066979272
|
||||
const resp = await fetch(
|
||||
`https://api.bilibili.com/x/player/v2?aid=${aid}&cid=${cid}`,
|
||||
commonConfig,
|
||||
);
|
||||
const subtitles = (await resp.json()).data.subtitle.subtitles;
|
||||
const subtitlesUrl = subtitles?.subtitle_url?.startsWith("//")
|
||||
? `https:${subtitles?.subtitle_url}`
|
||||
: subtitles?.subtitle_url;
|
||||
let inputText = "";
|
||||
logger.mark(subtitlesUrl);
|
||||
if (subtitlesUrl !== undefined) {
|
||||
const res = await fetch(subtitlesUrl);
|
||||
const subtitlesData = (await res.json()).body;
|
||||
const subtitleTimestamp = reduceBilibiliSubtitleTimestamp(
|
||||
subtitlesData,
|
||||
shouldShowTimestamp,
|
||||
);
|
||||
inputText = getSmallSizeTranscripts(subtitleTimestamp, subtitleTimestamp);
|
||||
} else {
|
||||
inputText = `${desc} ${dynamic}`;
|
||||
}
|
||||
const videoConfig = {
|
||||
showEmoji: true,
|
||||
};
|
||||
return shouldShowTimestamp
|
||||
? getUserSubtitleWithTimestampPrompt(title, inputText, videoConfig)
|
||||
: getUserSubtitlePrompt(title, inputText, videoConfig);
|
||||
}
|
||||
|
||||
// 以下拼接算法来自:https://github.com/JimmyLv/BibiGPT
|
||||
function reduceBilibiliSubtitleTimestamp(subtitles = [], shouldShowTimestamp) {
|
||||
return reduceSubtitleTimestamp(
|
||||
subtitles,
|
||||
i => i.from,
|
||||
i => i.content,
|
||||
shouldShowTimestamp,
|
||||
);
|
||||
}
|
||||
function reduceSubtitleTimestamp(subtitles, getStart, getText, shouldShowTimestamp) {
|
||||
// 把字幕数组总共分成 20 组
|
||||
const TOTAL_GROUP_COUNT = 30;
|
||||
// 如果字幕不够多,就每7句话合并一下
|
||||
const MINIMUM_COUNT_ONE_GROUP = 7;
|
||||
const eachGroupCount =
|
||||
subtitles.length > TOTAL_GROUP_COUNT
|
||||
? subtitles.length / TOTAL_GROUP_COUNT
|
||||
: MINIMUM_COUNT_ONE_GROUP;
|
||||
|
||||
return subtitles.reduce((accumulator, current, index) => {
|
||||
// 计算当前元素在哪一组
|
||||
const groupIndex = Math.floor(index / MINIMUM_COUNT_ONE_GROUP);
|
||||
|
||||
// 如果是当前组的第一个元素,初始化这一组的字符串
|
||||
if (!accumulator[groupIndex]) {
|
||||
accumulator[groupIndex] = {
|
||||
// 5.88 -> 5.9
|
||||
// text: current.start.toFixed() + ": ",
|
||||
index: groupIndex,
|
||||
s: getStart(current),
|
||||
text: shouldShowTimestamp ? getStart(current) + " - " : "",
|
||||
};
|
||||
}
|
||||
|
||||
// 将当前元素添加到当前组的字符串末尾
|
||||
accumulator[groupIndex].text = accumulator[groupIndex].text + getText(current) + " ";
|
||||
|
||||
return accumulator;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function getSmallSizeTranscripts(newTextData, oldTextData, byteLimit = 6200) {
|
||||
const text = newTextData
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map(t => t.text)
|
||||
.join(" ");
|
||||
const byteLength = getByteLength(text);
|
||||
|
||||
if (byteLength > byteLimit) {
|
||||
const filtedData = filterHalfRandomly(newTextData);
|
||||
return getSmallSizeTranscripts(filtedData, oldTextData, byteLimit);
|
||||
}
|
||||
|
||||
let resultData = newTextData.slice();
|
||||
let resultText = text;
|
||||
let lastByteLength = byteLength;
|
||||
|
||||
for (let i = 0; i < oldTextData.length; i++) {
|
||||
const obj = oldTextData[i];
|
||||
if (itemInIt(newTextData, obj.text)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextTextByteLength = getByteLength(obj.text);
|
||||
const isOverLimit = lastByteLength + nextTextByteLength > byteLimit;
|
||||
if (isOverLimit) {
|
||||
const overRate = (lastByteLength + nextTextByteLength - byteLimit) / nextTextByteLength;
|
||||
const chunkedText = obj.text.substring(0, Math.floor(obj.text.length * overRate));
|
||||
resultData.push({ text: chunkedText, index: obj.index });
|
||||
} else {
|
||||
resultData.push(obj);
|
||||
}
|
||||
resultText = resultData
|
||||
.sort((a, b) => a.index - b.index)
|
||||
.map(t => t.text)
|
||||
.join(" ");
|
||||
lastByteLength = getByteLength(resultText);
|
||||
}
|
||||
|
||||
return resultText;
|
||||
}
|
||||
|
||||
function filterHalfRandomly(arr) {
|
||||
const filteredArr = [];
|
||||
const halfLength = Math.floor(arr.length / 2);
|
||||
const indicesToFilter = new Set();
|
||||
|
||||
// 随机生成要过滤掉的元素的下标
|
||||
while (indicesToFilter.size < halfLength) {
|
||||
const index = Math.floor(Math.random() * arr.length);
|
||||
if (!indicesToFilter.has(index)) {
|
||||
indicesToFilter.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤掉要过滤的元素
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (!indicesToFilter.has(i)) {
|
||||
filteredArr.push(arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredArr;
|
||||
}
|
||||
|
||||
function getByteLength(text) {
|
||||
return unescape(encodeURIComponent(text)).length;
|
||||
}
|
||||
|
||||
function itemInIt(textData, text) {
|
||||
return textData.find(t => t.text === text) !== undefined;
|
||||
}
|
||||
|
||||
function getUserSubtitlePrompt(title, transcript, videoConfig) {
|
||||
const videoTitle = title?.replace(/\n+/g, " ").trim();
|
||||
const videoTranscript = limitTranscriptByteLength(transcript).replace(/\n+/g, " ").trim();
|
||||
const language = "zh-CN";
|
||||
const sentenceCount = videoConfig.sentenceNumber || 7;
|
||||
const emojiTemplateText = videoConfig.showEmoji ? "[Emoji] " : "";
|
||||
const emojiDescriptionText = videoConfig.showEmoji
|
||||
? "Choose an appropriate emoji for each bullet point. "
|
||||
: "";
|
||||
const shouldShowAsOutline = Number(videoConfig.outlineLevel) > 1;
|
||||
const wordsCount = videoConfig.detailLevel ? (Number(videoConfig.detailLevel) / 100) * 2 : 15;
|
||||
const outlineTemplateText = shouldShowAsOutline ? `\n - Child points` : "";
|
||||
const outlineDescriptionText = shouldShowAsOutline
|
||||
? `Use the outline list, which can have a hierarchical structure of up to ${videoConfig.outlineLevel} levels. `
|
||||
: "";
|
||||
const prompt = `Your output should use the following template:\n## Summary\n## Highlights\n- ${emojiTemplateText}Bulletpoint${outlineTemplateText}\n\nYour task is to summarise the text I have given you in up to ${sentenceCount} concise bullet points, starting with a short highlight, each bullet point is at least ${wordsCount} words. ${outlineDescriptionText}${emojiDescriptionText}Use the text above: {{Title}} {{Transcript}}.\n\nReply in ${language} Language.`;
|
||||
|
||||
return `Title: "${videoTitle}"\nTranscript: "${videoTranscript}"\n\nInstructions: ${prompt}`;
|
||||
}
|
||||
|
||||
export function getUserSubtitleWithTimestampPrompt(title, transcript, videoConfig) {
|
||||
const videoTitle = title?.replace(/\n+/g, " ").trim();
|
||||
const videoTranscript = limitTranscriptByteLength(transcript).replace(/\n+/g, " ").trim();
|
||||
const language = "zh-CN";
|
||||
const sentenceCount = videoConfig.sentenceNumber || 7;
|
||||
const emojiTemplateText = videoConfig.showEmoji ? "[Emoji] " : "";
|
||||
const wordsCount = videoConfig.detailLevel ? (Number(videoConfig.detailLevel) / 100) * 2 : 15;
|
||||
const promptWithTimestamp = `Act as the author and provide exactly ${sentenceCount} bullet points for the text transcript given in the format [seconds] - [text] \nMake sure that:\n - Please start by summarizing the whole video in one short sentence\n - Then, please summarize with each bullet_point is at least ${wordsCount} words\n - each bullet_point start with \"- \" or a number or a bullet point symbol\n - each bullet_point should has the start timestamp, use this template: - seconds - ${emojiTemplateText}[bullet_point]\n - there may be typos in the subtitles, please correct them\n - Reply all in ${language} Language.`;
|
||||
const videoTranscripts = limitTranscriptByteLength(JSON.stringify(videoTranscript));
|
||||
return `Title: ${videoTitle}\nTranscript: ${videoTranscripts}\n\nInstructions: ${promptWithTimestamp}`;
|
||||
}
|
||||
|
||||
function limitTranscriptByteLength(str, byteLimit = 6200) {
|
||||
const utf8str = unescape(encodeURIComponent(str));
|
||||
const byteLength = utf8str.length;
|
||||
if (byteLength > byteLimit) {
|
||||
const ratio = byteLimit / byteLength;
|
||||
const newStr = str.substring(0, Math.floor(str.length * ratio));
|
||||
return newStr;
|
||||
}
|
||||
return str;
|
||||
}
|
@ -65,6 +65,33 @@ async function getDownloadUrl (url) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getAudioUrl (url) {
|
||||
return axios
|
||||
.get(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
|
||||
referer: 'https://www.bilibili.com',
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const info = JSON.parse(
|
||||
data.match(/<script>window\.__playinfo__=({.*})<\/script><script>/)?.[1],
|
||||
);
|
||||
// 获取音频
|
||||
const audioUrl =
|
||||
info?.data?.dash?.audio?.[0]?.baseUrl ?? info?.data?.dash?.audio?.[0]?.backupUrl?.[0];
|
||||
const title = data.match(/title="(.*?)"/)?.[1]?.replaceAll?.(/\\|\/|:|\*|\?|"|<|>|\|/g, '');
|
||||
|
||||
|
||||
if (audioUrl) {
|
||||
return { audioUrl, title };
|
||||
}
|
||||
|
||||
return Promise.reject('获取下载地址失败');
|
||||
});
|
||||
}
|
||||
|
||||
async function mergeFileToMp4 (vFullFileName, aFullFileName, outputFileName, shouldDelete = true) {
|
||||
// 判断当前环境
|
||||
let env;
|
||||
@ -95,4 +122,4 @@ async function mergeFileToMp4 (vFullFileName, aFullFileName, outputFileName, sho
|
||||
}
|
||||
}
|
||||
|
||||
export { downloadBFile, getDownloadUrl, mergeFileToMp4 }
|
||||
export { downloadBFile, getDownloadUrl, getAudioUrl, mergeFileToMp4 }
|
||||
|
@ -4,6 +4,7 @@ import axios from "axios";
|
||||
import fs from "node:fs";
|
||||
import fetch from "node-fetch";
|
||||
import { mkdirIfNotExists } from "./file.js";
|
||||
import {TEN_THOUSAND} from "../constants/constant.js";
|
||||
|
||||
/**
|
||||
* 请求模板
|
||||
@ -163,4 +164,41 @@ async function downloadMp3(mp3Url, path, redirect = "manual") {
|
||||
});
|
||||
}
|
||||
|
||||
export { jFeatch, autoTask, retry, getIdVideo, generateRandomStr, downloadMp3 };
|
||||
/**
|
||||
* 千位数的数据处理
|
||||
* @param data
|
||||
* @return {string|*}
|
||||
*/
|
||||
const dataProcessing = data => {
|
||||
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + "万" : data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 哔哩哔哩解析的数据处理
|
||||
* @param data
|
||||
* @return {string}
|
||||
*/
|
||||
function formatBiliInfo(data) {
|
||||
return Object.keys(data).map(key => `${key}:${dataProcessing(data[key])}`).join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字转换成具体时间
|
||||
* @param seconds
|
||||
* @return {string}
|
||||
*/
|
||||
function secondsToTime(seconds) {
|
||||
const pad = (num, size) => num.toString().padStart(size, '0');
|
||||
|
||||
let hours = Math.floor(seconds / 3600);
|
||||
let minutes = Math.floor((seconds % 3600) / 60);
|
||||
let secs = seconds % 60;
|
||||
|
||||
// 如果你只需要分钟和秒钟,你可以返回下面这行:
|
||||
// return `${pad(minutes, 2)}:${pad(secs, 2)}`;
|
||||
|
||||
// 完整的 HH:MM:SS 格式
|
||||
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(secs, 2)}`;
|
||||
}
|
||||
|
||||
export { jFeatch, autoTask, retry, getIdVideo, generateRandomStr, downloadMp3, dataProcessing, formatBiliInfo, secondsToTime };
|
||||
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* 用于百度翻译的常量控制
|
||||
*
|
||||
* @type {{英: string, 日: string, 文: string, 中: string}}
|
||||
*/
|
||||
export const transMap = { 中: "zh", 日: "jp", 文: "wyw", 英: "en", 俄: "ru", 韩: "kr" };
|
||||
/**
|
||||
* 用于腾讯交互式翻译的常量控制
|
||||
*
|
||||
* @type {{英: string, 俄: string, 日: string, 韩: string, 中: string}}
|
||||
*/
|
||||
export const tencentTransMap = { 中: "zh", 日: "ja", 韩: "ko", 英: "en", 俄: "ru" };
|
||||
/**
|
||||
* 用于腾讯交互式翻译的常量控制
|
||||
*
|
||||
* @type {{英: string, 俄: string, 日: string, 韩: string, 中: string}}
|
||||
*/
|
||||
export const googleTransMap = { 中: "zh-CN", 日: "jp", 韩: "ko", 英: "en", 俄: "ru" };
|
||||
|
||||
/**
|
||||
* 以下为抖音/TikTok类型代码
|
||||
*
|
||||
* @type {{"0": string, "55": string, "2": string, "68": string, "58": string, "4": string, "61": string, "51": string, "150": string}}
|
||||
*/
|
||||
export const douyinTypeMap = {
|
||||
2: "image",
|
||||
4: "video",
|
||||
68: "image",
|
||||
0: "video",
|
||||
51: "video",
|
||||
55: "video",
|
||||
58: "video",
|
||||
61: "video",
|
||||
150: "image",
|
||||
};
|
||||
|
||||
export const TEN_THOUSAND = 10000;
|
||||
|
||||
export const CAT_LIMIT = 10;
|
||||
|
||||
export const XHS_CK = 'eGhzVHJhY2tlcklkPTczODhhYmY2LTI0MDgtNGU5YS04MTUyLTE0MGVhOGY1MTQ5ZjsgeGhzVHJhY2tlcklkLnNpZz1UcGUxTkNaX3B3UkFYdG01SVJmVEs0SWUxM0xBaGZuNmNZU2N4Vi1JYWxFOyBhMT0xODY2ZDkwMDM0NmI2NmppcjMzcGpxZ2MwM3JvcG1mczAydXMxdWNoeDEwMDAwMTM1MDUzOyB3ZWJJZD1mMTNkOGJkYjhiZGM3ZGE0MzY0NjA4NWJjYzQ1MDQ1YTsgZ2lkPXlZS0tmajg4SzA4MnlZS0tmajg4cUo3UzRLREtLVjNGcXFVVjd4Q0FrUzhxRk15OGxVNmlNeTg4OHlxMjgycThmMlk0UzAySjsgZ2lkLnNpZ249YlpzcFFzSUxEUmN5akZLQmN2L1FMWVhkU3lvPTsgd2ViX3Nlc3Npb249MDMwMDM3YTRjMDQyYjE1ZTVjMTg4OTUwOGIyNDRhZDExM2UwNTM7IHhoc1RyYWNrZXI9dXJsPW5vdGVEZXRhaWwmeGhzc2hhcmU9V2VpeGluU2Vzc2lvbjsgeGhzVHJhY2tlci5zaWc9YzdmcDVRclk2SGNvVERhUzluX2N3Z2RCRHh2MFZmWnpSU1NTcnlzbG5lQTsgZXh0cmFfZXhwX2lkcz1oNV8yMzAyMDExX29yaWdpbixoNV8xMjA4X2NsdCxoNV8xMTMwX2NsdCxpb3Nfd3hfbGF1bmNoX29wZW5fYXBwX2V4cCxoNV92aWRlb191aV9leHAzLHd4X2xhdW5jaF9vcGVuX2FwcF9kdXJhdGlvbl9vcmlnaW4scXVlc19jbHQyOyBleHRyYV9leHBfaWRzLnNpZz1DVUdrR3NYT3lBZmpVSXkyVGo3SjN4YmRNakFfSnpoR1JkYWd6cVlkbmJnOyB3ZWJCdWlsZD0xLjEuMjE7IHhzZWNhcHBpZD14aHMtcGMtd2ViOyB3ZWJzZWN0aWdhPTU5ZDNlZjFlNjBjNGFhMzdhN2RmM2MyMzQ2N2JkNDZkN2YxZGEwYjE5MThjZjMzNWVlN2YyZTllNTJhYzA0Y2Y7IHNlY19wb2lzb25faWQ9MTI0OTE1NWQtOWU5ZS00MzkyLTg2NTgtNTA1Yzc0YTUzMTM1'
|
@ -1,4 +1,4 @@
|
||||
import {transMap, tencentTransMap, googleTransMap} from "./constant.js";
|
||||
import {transMap, tencentTransMap, googleTransMap} from "../constants/constant.js";
|
||||
import md5 from "md5";
|
||||
import fetch from "node-fetch";
|
||||
import HttpProxyAgent from "https-proxy-agent";
|
||||
|
Loading…
x
Reference in New Issue
Block a user