1、前言
近期要做个智能文案工具,来帮助电商公司的运营同学提效。包括AI创作、风格改写、文案续写、文案提取、智能问答等功能的运营文案创作助手。
其中AI创作、风格改写啊,实现过程比较简单,基本上就是把用户的想法,包括主题、观点传给文本大模型即可,通过限定大模型系统提示词,加上外挂电商营销文案知识库,一般大模型会给出符合主题和观点的文案。
在文案提取功能上遇到了卡点,我们希望能提取视频中的营销文案内容。在实现前简单的理解为文本大模型可以直接识别在线的视频链接,然后给出视频文案。实际上不是的。问了下DeepSeek、元宝、Kimi等都是给出"无法直接访问XX或其他外部链接的内容"。其中百度网页版也能分析出点内容,但是和我们想要的视频文案差距太大。
-
DeepSeek(网页版)
-
Baidu(DeepSeek-R1网页版)
2、需求说明
2.1 需求说明
我们希望能提取出视频链接的原始文案,而不是对视频的内容分析,视频的理解,视频的标题等无关内容。
因为有了原始文案,我们可以对文案进行二创、润色、风格改写等动作。
同样的我们也能够获取视频分析、视频标题、视频标签等信息。
2.2 数据准备
- 三方平台视频链接
// 精选链接:
https://www.douyin.com/jingxuan?modal_id=7359544025726143771
// 详情页链接
https://www.douyin.com/video/7359544025726143771
- 视频文案如下
贫穷真的跟懒惰有关系吗?江秦这些年一直在思考这个问题。他觉得自己已经足够勤奋了,完全对得起自己的名字。可钱呢?钱到底是被谁给赚走了?小时候,爸妈曾语重心长地告诉他,只要你肯吃苦,就一定会出人头地。但他长大后发现的事实却是,只要你肯吃苦,就一定有吃不完的苦。现在,他的相亲对象要彩礼三十万。江秦,你有没有听我说话?嗯,我一直听着呢。那你怎么一声也不吭?我都说了半天,嗓子都哑了,你也不管。江秦放下水杯,沉默半晌后开口:这婚要不还是别结了吧。女人愣了一下,随即勃然大怒:你这话是什么意思?没什么,就是觉得好累,想回家睡一觉。江秦,你个孬种,怪不得你都三十八了,也没有女人想跟你。江秦不顾女人的咆哮,迈步走出了西餐厅,沿着马路漫无目的地往前走去。走到一个建筑工地的时候,他看到围墙上挂着一条横幅,写着:打工人是人上人。于是,他点上根烟,吧嗒抽了两口后,在在上面烫了个洞。他对那个女人其实没有太多的怨言,甚至觉得她的要求很正常。人家都三十五了,现实一点有什么毛病?他只是在思考一个问题,这样的日子哪一天是个尽头。没打过工的人拼命鼓吹着打工人是人上人,一直在打工的人却什么都不敢说,只能点头承认:啊,对对对。可自己到底哪里像个人上人?这辈子就混了两双爱意还是莆田的,你管这叫人上人?至于爱情,江秦甚至都不知道这东西存不存在。他相过几次亲,见过几个朋友介绍的女孩,无论哪个都可以凑合过,但最悲哀的也是仅限于凑合过。回顾一生,这辈子的遗憾真的太多了。江秦叹了口气,从口袋里摸出电话,想找个朋友陪自己喝点酒,但点菜后却看到了四条短信:一条信用卡催款通知,一条话费欠费预警,一条哥哥我在附近,今天家里没有人,最后一条来自他的直属领导,用语重心长的文字跟他说:最近公司效益不好,希望员工可以自愿降薪,与公司一起共渡难关。江秦瞬间失去了喝酒的心情,继续在施工楼下抽着烟。在这个时代,你想要有钱就绝对不能打工,因为这个社会的资源分配本来就是不公平的。可是一想到自己的年纪,江秦忍不住笑了。三十八了,再去创业有点不现实吧?他这两年腰都累断了,颈椎也出问题了,交叉神经痛比尿频还勤快。拖着这残破的身躯去创业,就算成功也得五十岁了。这人生还有什么可享受的?要是能重来就好了,打什么都别打工,能傍富婆就傍富婆,实在不行就创业,坚信钱没了可以再赚,可良心没了赚的更多。
3、功能实现
3.1 使用视频理解大模型能力
-
思路
把视频链接传给大模型,让大模型识别视频文案并输出。 -
视频理解大模型原理
对视频文件每隔0.5秒抽取一帧,采用图像理解技术识别每帧图像信息,通过图像分析间接实现视频内容分析的。
允许设置fps参数控制抽帧频率,高速运动场景比如体育赛事、动作电影适合较高的fps;长视频或内容偏静态视频适合较低的fps。 -
以通义千问VL模型为例
- 文件形式
- 在线视频链接要求:视频链接是公网访问且没有权限拦截的,文件是常见的视频文件格式。否则无法获取视频内容,就无法做视频理解。
- 支持使用本地文件:通过SDK允许使用本地文件。需要将本地文件编码为Base64格式,或者直接传入本地路径。
- 文件限制
- 视频文件大小:Qwen2.5-VL系列模型支持传入的视频大小不超过1 GB,其他模型不超过150MB
- 视频文件格式: MP4、AVI、MKV、MOV、FLV、WMV 等。
- 视频时长:Qwen2.5-VL系列模型支持的视频时长为2秒至10分钟,其他模型为2秒至40秒。
- 文件形式
-
使用示例
curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen-vl-max-latest",
"messages": [
{"role": "system", "content": [{"type": "text","text": "You are a helpful assistant."}]},
{"role": "user","content": [{"type": "video_url","video_url": {"url": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241115/cqqkru/1.mp4"}},
{"type": "text","text": "这段视频的内容是什么?"}]}]
}'
3.1.1 三方平台视频在线链接解析
直接拿着三方平台视频链接,因三方平台官方安全策略,大模型无法识别,提示传入的视频文件无效。
// 请求信息
{
"enable_thinking": false,
"max_tokens": 4096,
"messages": [
{
"content": [
{
"type": "video_url",
"video_url": {"url": "https://www.douyin.com/video/7359544025726143771"}
},
{
"type": "text","text": "请提取视频文案"
}
],
"role": "user"
}
],
"model": "qwen-vl-max",
"stream": false,
"temperature": 0.7
}
// 响应结果
{
"error": {
"code": "invalid_parameter_error",
"param": null,
"message": "<400> InternalError.Algo.InvalidParameter: Invalid video file.",
"type": "invalid_request_error"
},
"id": "chatcmpl-94d36a39-3322-9fbd-9b8f-2dedc1850d21",
"request_id": "94d36a39-3322-9fbd-9b8f-2dedc1850d21"
}
3.1.2 三方平台视频内网链接解析
三方平台视频内网链接是通过三方平台视频详情接口 /aweme/v1/web/aweme/detail 获取的,如下所示,调用用法在下面音频识别模块会介绍。
获取到三方平台视频内网链接,大模型无法识别,提示链接资源无法下载或下载超时。
// 请求信息
{
"enable_thinking": false,
"max_tokens": 4096,
"messages": [
{
"content": [
{
"type": "video_url",
"video_url": {"url": "https://v3-web.douyinvod.com/bf9cca29836d3612cc035943bc6e220c/685a7e6e/video/tos/cn/tos-cn-ve-15/ocQjQeefGBgQLKqB3QIodOIZGCEEbNMAPm7anA/?a=6383&ch=26&cr=3&dr=0&lr=all&cd=0%7C0%7C0%7C3&cv=1&br=5029&bt=5029&cs=0&ds=4&ft=AJkeU_TLRR0sTlC42Dv2Nc.xBiGNbLMY4jdU_45JCAxJNv7TGW&mime_type=video_mp4&qs=0&rc=NzM4OTVkaDc4N2gzZjtnOkBpanhuaW05cnhoMzMzNGkzM0BjL2IyLTViXi8xLV5hYWA0YSMzYHJvMmRzbTNhLS1kLS9zcw%3D%3D&btag=80000e00010000&cquery=100B_100x_100z_100o_100w&dy_q=1750750244&feature_id=59cb2766d89ae6284516c6a254e9fb61&l=20250624153044F89E8D93AC3D0D87B3C8"}
},
{
"type": "text","text": "请提取视频文案"
}
],
"role": "user"
}
],
"model": "qwen-vl-max",
"stream": false,
"temperature": 0.7
}
// 响应结果
{
"error": {
"code": "invalid_parameter_error",
"param": null,
"message": "<400> InternalError.Algo.InvalidParameter: Failed to download multimodal content",
"type": "invalid_request_error"
},
"id": "chatcmpl-84b97fed-33ad-9d98-a40a-b893fb64c969",
"request_id": "84b97fed-33ad-9d98-a40a-b893fb64c969"
}
具体错误信息如下:https://help.aliyun.com/zh/model-studio/error-code
- 网络原因,请检查您的网络连接是否正常。
- 该文件的URL为OSS的内网URL。由于OSS内网与阿里云百炼服务不互通,请勿使用OSS内网URL。
- 提供的图片资源所在的IP地址不在中国内地。
- 由于网络环境的差异,跨境资源访问可能会受到一定的限制或不稳定因素影响。建议您尽量使用中国内地的资源存储服务,以确保网络连接的稳定性和访问速度。
3.1.3 三方平台视频转存本地服务
通过以上两种方式,看出三方平台视频链接因三方平台官方的安全策略,无法直接识别。
可以下载视频到自己服务器上或者下载并转存到cos、oos云服务器上,获取到oss链接再交给大模型进行解析。
此处就不再演示。
3.2 使用音频识别大模型能力
-
思路
获取视频的音频文件,把音频文件链接传给大模型,让大模型识别音频文案并输出。
支持多种音频(包括说话人语音、自然声音、音乐、歌声)和文本作为输入,并输出文本。不仅能对输入的音频进行转录,还具备更深层次的语义理解、情感分析、音频事件检测、语音聊天等能力。 -
以通义千问Audio模型为例
- 文件形式
- 在线音频链接要求:音频链接是公网访问且没有权限拦截的,文件是常见的音频文件格式。
- 支持使用本地文件:通过SDK允许使用本地文件。需要传入本地音频的绝对路径。
- 文件限制
- 音频文件大小:建议不超过10 MB,超出也是可以解析的
- 音频文件格式: AMR、WAV(CodecID: GSM_MS)、WAV(PCM)、3GP、3GPP、AAC、MP3等。
- 音频时长:音频的时长建议不超过30秒,如果超过30秒,模型会自动截取前30秒的音频。实际上解析2min时长的音频也是可以的
- 音频语言:中文、英语、粤语、法语、意大利语、西班牙语、德语和日语
- 文件形式
-
使用示例
curl -X POST https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation \
-H "Authorization: Bearer $DASHSCOPE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen-audio-turbo-latest",
"input":{
"messages":[
{
"role": "system",
"content": [
{"text": "You are a helpful assistant."}
]
},
{
"role": "user",
"content": [
{"audio": "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/welcome.mp3"},
{"text": "这段音频在说什么?"}
]
}
]
}
}'
3.2.1 三方平台视频在线链接解析
通过浏览器访问三方平台视频详情页链接,打开浏览器控制台,可以看到有一个视频详情接口 /aweme/v1/web/aweme/detail 的调用,返回了视频的明细,包括视频链接、音频链接、视频标题、视频标签、ocr识别文案、分享信息等。
下面我找了一写关键信息贴出来。
其中我们发现有ocr识别结果 aweme_detail.seo_info.ocr_content,但是文案质量不佳,封面或者视频背景中的特殊字符文案也会识别出来。
说明走ocr识别方案可能也不是我们想要的视频文案结果。
{
"aweme_detail": {
"caption": "#重生文 #AIGC #错哪儿了 都重生了谁谈恋爱啊",
"desc": "#重生文 #AIGC #错哪儿了 都重生了谁谈恋爱啊",
"music": {
"play_url": {
"height": 720,
"uri": "https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7359544061390326578.mp3",
"url_key": "7359544062350822195",
"url_list": [
"https://sf3-cdn-tos.douyinstatic.com/obj/ies-music/7359544061390326578.mp3",
"https://sf6-cdn-tos.douyinstatic.com/obj/ies-music/7359544061390326578.mp3"
],
"width": 720
}
},
"preview_title": "#重生文 #AIGC #错哪儿了 都重生了谁谈恋爱啊",
"seo_info": {
"ocr_content": "起点读书 谁谈恋爱啊 吗 钱到底是被谁给赚走了 地告诉他 出人头地 却是 只要你肯吃苦就一定有 吃不完的苦 现在 十万 ”“嗯 我都说了半天 嗓子都哑了你也不管 沉默半晌后开口:“这 婚 aui 话是什么意思”“ E R4C 想回家睡一觉”“江勤 你个孬种 前走去 1001010 沿着马路漫无目的地往 候 横幅 吧嗒抽了两口后在上面 烫了个洞 太多的怨言 常 人家都三十五了 尽头 着 写着打工人是人上人 打工人是人上人 啊对对对 上人 还是莆田的 至于爱情 西存不存在 见过几个朋友介绍的女 孩 凑合过 回顾一生 这辈子的遗憾真的太多 奋了 酒 但点开后却看到了四条 短信 领导 说 有 心情 i 配 可是一想到自己的年纪 可是一想到自己的年纪 江勤瞬间失去了喝酒的 江勤忍不住笑了 不现实吧 乐 他只是在思考一个问题 交叉神经痛比尿频还勤 快 业 的 打工 打什么都不打工 都不敢说 该内容引用AI能力生产 可良心没了赚的更多"
},
"share_info": {
"share_desc": "在XX,记录美好生活",
"share_desc_info": "#在XX,记录美好生活##重生文 #AIGC #错哪儿了 都重生了谁谈恋爱啊",
"share_link_desc": "3.02 Q@X.zT 03/05 mqE:/ # 重生文 # AIGC # 错哪儿了 都重生了谁谈恋爱啊 %s 复制此链接,打开Dou音搜索,直接观看视频!",
"share_url": "https://www.iesdouyin.com/share/video/7359544025726143771/?region=CN\u0026mid=7359544062350822195\u0026u_code=353j2f7d77mc\u0026did=MS4wLjABAAAAqHqcov8rpj4LVZ8iF07s0MNfkPzs3ytKRY7R1ioqIPBXNmjExJVCCn98dkv4GFyX\u0026iid=MS4wLjABAAAANwkJuWIRFOzg5uCpDRpMj4OX-QryoDgn-yYlXQnRwQQ\u0026with_sec_did=1\u0026video_share_track_ver=\u0026titleType=title\u0026share_sign=IbKCVSSsLU6Xp_9LStddXTQcO8hqW30VeXjQ_PnB16A-\u0026share_version=190500\u0026ts=1750821377\u0026from_aid=6383\u0026from_ssr=1"
},
"text_extra": [
{
"caption_end": 4,
"caption_start": 0,
"end": 4,
"hashtag_id": "1608221011432477",
"hashtag_name": "重生文",
"is_commerce": false,
"start": 0,
"type": 1
},
{
"caption_end": 10,
"caption_start": 5,
"end": 10,
"hashtag_id": "1722895911293971",
"hashtag_name": "aigc",
"is_commerce": false,
"start": 5,
"type": 1
},
{
"caption_end": 16,
"caption_start": 11,
"end": 16,
"hashtag_id": "1631690698606605",
"hashtag_name": "错哪儿了",
"is_commerce": false,
"start": 11,
"type": 1
}
],
"video": {
"play_addr": {
"data_size": 10329912,
"file_cs": "c:0-172367-aa4c",
"file_hash": "39d7feda1c7822ee2a3b4f3b3ce18240",
"height": 960,
"uri": "v0200fg10000coh5k7rc77u15ncs6cag",
"url_key": "v0200fg10000coh5k7rc77u15ncs6cag_h264_540p_403786",
"url_list": [
"https://v26-web.douyinvod.com/7bd3b24c1e6ca49a04eb7661c65ff0bd/685b94fd/video/tos/cn/tos-cn-ve-0015c800/oczi2CcPEBDEnIhCmteWA1BAb07VSgMdQLAJfz/?a=6383\u0026ch=26\u0026cr=3\u0026dr=0\u0026lr=all\u0026cd=0%7C0%7C0%7C3\u0026cv=1\u0026br=394\u0026bt=394\u0026cs=0\u0026ds=6\u0026ft=AJkeU_TLRR0sTlC42Dv2Nc.xBiGNbLjf~jdU_45JCAxJNv7TGW\u0026mime_type=video_mp4\u0026qs=0\u0026rc=ZDs2Mzk7Zmg1PGc0Njo1OUBpamZ2cTQ6Zjo4cjMzNGkzM0BhMDEzLS9eNTYxYmExXl8wYSNeM2AycjRvaGVgLS1kLS9zcw%3D%3D\u0026btag=80000e00028000\u0026dy_q=1750821377\u0026feature_id=f0150a16a324336cda5d6dd0b69ed299\u0026l=202506251116174010AE3B891530B28EB0",
"https://v3-web.douyinvod.com/ae0626098d54eda9e992e48653624ea3/685b94fd/video/tos/cn/tos-cn-ve-0015c800/oczi2CcPEBDEnIhCmteWA1BAb07VSgMdQLAJfz/?a=6383\u0026ch=26\u0026cr=3\u0026dr=0\u0026lr=all\u0026cd=0%7C0%7C0%7C3\u0026cv=1\u0026br=394\u0026bt=394\u0026cs=0\u0026ds=6\u0026ft=AJkeU_TLRR0sTlC42Dv2Nc.xBiGNbLjf~jdU_45JCAxJNv7TGW\u0026mime_type=video_mp4\u0026qs=0\u0026rc=ZDs2Mzk7Zmg1PGc0Njo1OUBpamZ2cTQ6Zjo4cjMzNGkzM0BhMDEzLS9eNTYxYmExXl8wYSNeM2AycjRvaGVgLS1kLS9zcw%3D%3D\u0026btag=80000e00028000\u0026dy_q=1750821377\u0026feature_id=f0150a16a324336cda5d6dd0b69ed299\u0026l=202506251116174010AE3B891530B28EB0",
"https://www.douyin.com/aweme/v1/play/?video_id=v0200fg10000coh5k7rc77u15ncs6cag\u0026line=0\u0026file_id=36b2a743d1ed49928eac2806604338da\u0026sign=39d7feda1c7822ee2a3b4f3b3ce18240\u0026is_play_url=1\u0026source=PackSourceEnum_AWEME_DETAIL"
],
"width": 544
},
"ratio": "540p",
"video_model": "",
"width": 544
},
"video_tag": [
{
"level": 1,
"tag_id": 2014,
"tag_name": "二次元"
},
{
"level": 2,
"tag_id": 2014002,
"tag_name": "二次元内容"
},
{
"level": 3,
"tag_id": 2014002001,
"tag_name": "动漫IP"
}
]
}
3.2.2 三方平台视频详情接口说明
三方平台视频详情接口 /aweme/v1/web/aweme/detail,该接口目前可通过以下两个域名访问。
- 第一个域名是我们访问三方平台视频详情页链接,在浏览器控制台获取到的
https://www-hj.douyin.com/aweme/v1/web/aweme/detail/
- 第二个域名是我们通过访问iframe嵌套的
https://www.douyin.com/aweme/v1/web/aweme/detail/
- 通过VideoID获取IFrame代码
curl --location --request GET 'https://open.douyin.com/api/douyin/v1/video/get_iframe_by_video?video_id=7359544025726143771'
响应结果:
{
"log_id" : "202506250953238DD93A552BB7E8653C75",
"err_msg" : "",
"err_no" : 0,
"data" : {
"iframe_code" : "<iframe width=\"544\" height=\"960\" frameborder=\"0\" src=\"https://open.douyin.com/player/video?vid=7359544025726143771&autoplay=0\" referrerpolicy=\"unsafe-url\" allowfullscreen></iframe>",
"video_height" : 960,
"video_title" : "#重生文 #AIGC #错哪儿了 都重生了谁谈恋爱啊",
"video_width" : 544
}
}
3.2.3 通过三方平台视频详情获取音频链接
- 请求头配置
此处使用https://www.douyin.com/aweme/v1/web/aweme/detail/域名接口,注意该接口的请求头需要配置
Origin: https://open.douyin.com
Referer: https://open.douyin.com
如果使用https://www-hj.douyin.com/aweme/v1/web/aweme/detail/域名接口,注意该接口的请求头需要配置
Origin: https://www.douyin.com
Referer: https://www.douyin.com
- 请求参数说明
此处使用https://www.douyin.com/aweme/v1/web/aweme/detail/
核心参数aweme_id传视频ID
核心参数aid传固定值,用浏览器控制台抓取到的那个值就可以
其他参数msToken、X-Bogus、_signature是三方平台官方安全策略参数,用官方的js文件可以找到加密方法,此处固定用浏览器控制台抓取到的值也可以。 - DyDetailVO.java
请求结果VO封装
package com.adtool.platform.controller.vo.wenan;
import lombok.Data;
import java.util.List;
/**
* @className: DyDetailVO
* @description: 三方平台视频明细
* @author: author
* @date: 2025/6/24 18:24
**/
@Data
public class DyDetailVO {
private AwemeDetail aweme_detail;
private LogPb log_pb;
private Integer status_code;
@Data
public static class LogPb {
private String impr_id;
}
@Data
public static class AwemeDetail {
/** 视频描述 */
private String desc;
private Double duration;
/** 视频标题 */
private String item_title;
/** 音乐信息 */
private Music music;
/** 预览标题 */
private String preview_title;
/** seo信息 */
private SeoInfo seo_info;
/** 标题标签 */
private List<TextExtra> text_extra;
private Video video;
/** 视频标签 */
private List<VideoTag> video_tag;
@Data
public static class Music {
/** 音乐名称 */
private PlayUrl play_url;
@Data
public static class PlayUrl {
/** 音频地址 */
private String uri;
}
}
@Data
public static class SeoInfo {
/** ocr_content */
private String ocr_content;
}
@Data
public static class TextExtra {
/** 标签名称 */
private String hashtag_name;
}
@Data
public static class Video {
/** 格式 */
private String format;
/** 分辨率 */
private String ratio;
private PlayAddr play_addr;
@Data
public static class PlayAddr {
/**
* 播放地址,取list.get(0)
* 对帧率有要求可以取其他值
* play_addr
* play_addr_265
* play_addr_h264
*/
private List<String> url_list;
}
}
@Data
public static class VideoTag {
/** 标签名称 */
private String tag_name;
}
}
}
- 获取三方平台视频明细
请求过程是crul调用,这里就不写了。
@Value("${dy.video.detail:https://www.douyin.com/aweme/v1/web/aweme/detail/?aweme_id={{videoId}}&aid=6383&msToken=KbscjTT6O_4LM5GZSm6ulplDk6kFy5lvsIznwgVdhWUng75b2NqTLC4lnKwENN1uiW52Ub2Q1P3yUS6GL9EUSNIDSqgiH7k5uGiDGYvrjt9YgpYRJKvqQw==&X-Bogus=DFSzswVOW7iANrWECCcE6QTQh4SC&_signature=_02B4Z6wo00001qQHOyAAAIDDe1zIbmL9oZKkBz-AAMFn8sgvQzXK4sUcPGou9UVpawJQumggtRNzYPluyC5lYZv1kLaElZP-aNIeKLcgp7x7moMOtAYAC6ggXbhscNqWEePdp0-JKSpODhxd6e}")
private String getDyAwemeDetail;
public void videoLinkAnalysis(String videoId) {
String url = getDyAwemeDetail.replace(Constants.VIDEO_ID, videoId);
Map<String, String> headers = new HashMap<>(2);
headers.put("Origin".intern(), "https://open.douyin.com".intern());
headers.put("Referer".intern(), "https://open.douyin.com".intern());
Response response = httpClientService.buildResponseGet(url, videoId, headers);
if (response.isSuccessful()) {
try {
dyDetailVO = JSON.parseObject(response.body().string(), DyDetailVO.class);
} catch (IOException e) {
log.error("获取视频明细失败,请稍后重试!", e);
throw new RuntimeException("获取视频明细失败,请稍后重试!");
}
}
String musicUrl = dyDetailVO.getAweme_detail().getMusic().getPlay_url().getUri();
// TODO 获取到音频链接 ...
}
3.2.4 通过音频链接获取视频文案
此处以Crul形式调用通义千问Audio模型。
实现效果如下:
下面是部分实现代码。
- OpenAIApiService.java
大模型请求实现类核心方法,流式解析大模型识别结果。
private static final String STREAM_MESSAGE_PREFIX_NO_EMPTY = "data:";
private static final String STOP = "stop";
@Resource
private ExecutorService chatRequestExecutor;
@Resource
private ChatEngineRetryService chatEngineRetryService;
/**
* @param: chatInput
* @param: emitter
* @param: openaiUrl
* @param: key
* data:{"output":{"choices":[{"message":{"content":[{"text":"音频"}],"role":"assistant"},"finish_reason":"null"}]},"usage":{"audio_tokens":754,"input_tokens":785,"output_tokens":1},"request_id":"972092ff-3184-9aad-bfc0-8aa2a34a4f25"}
* data:{"output":{"choices":[{"message":{"content":[],"role":"assistant"},"finish_reason":"stop"}]},"usage":{"audio_tokens":754,"input_tokens":785,"output_tokens":236},"request_id":"2537f174-7767-9680-91fd-73230ff14cd4"}
* @return: void
* @author: 音频大模型流式接口
* @date: 2025/6/24
*/
@Override
public void streamIncrementalApi(ChatInput<String> chatInput, SseEmitter emitter, String openaiUrl, String key) {
chatRequestExecutor.execute(() -> {
long start = System.currentTimeMillis();
try (Response response = buildResponse(chatInput, openaiUrl, key)) {
log.info("API耗时响应: {}s", (System.currentTimeMillis()-start)/1000);
try (InputStream is = response.body().byteStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader bufferedReader = new BufferedReader(isr)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
log.info("res stream:{}", line);
if (!line.contains(STREAM_MESSAGE_PREFIX_NO_EMPTY)){
continue;
}
String messageJsonStr = line.substring(line.indexOf(STREAM_MESSAGE_PREFIX_NO_EMPTY) + STREAM_MESSAGE_PREFIX_NO_EMPTY.length());
OpenAIRes res = JSONUtil.toBean(messageJsonStr, OpenAIRes.class);
Choice choice = res.getOutput().getChoices().get(0);
if (STOP.equals(choice.getFinish_reason())) {
emitter.send(new GptStreamDto(1, StringUtils.EMPTY, false, true));
continue;
}
List<MessageContent> content = JSONUtil.toList((JSONArray) choice.getMessage().getContent(), MessageContent.class);
if (!CollectionUtils.isEmpty(content)) {
emitter.send(new GptStreamDto(1, content.get(0).getText(), false, false));
}
}
}
} catch (Exception e) {
log.error("流处理异常", e);
} finally {
log.info("API耗时流式对话: {}s", (System.currentTimeMillis()-start)/1000);
emitter.complete();
}
});
}
/**
* @param: chatInput
* @param: openaiUrl
* @param: key
* @description: 构建OkClient请求结果
* @return: okhttp3.Response
* @author: niaonao
* @date: 2025/6/25
*/
@Override
public Response buildResponse(Object chatInput, String openaiUrl, String key) {
String body = com.alibaba.fastjson.JSON.toJSONString(chatInput);
RequestBody requestBody = RequestBody.create(body, JSON);
/**
* 如果服务端接口本身不支持流式响应(SSE),即使客户端设置 X-DashScope-SSE: enable,服务端会忽略该头部并按默认非流式模式返回数据。
* 主流API设计规范中,非流式接口会直接丢弃无关的流式控制头部,不会引发错误。
*/
Request requestOpenai = new Request.Builder()
.url(openaiUrl)
.post(requestBody)
.addHeader("Authorization", "Bearer " + key)
.addHeader("api-key", key)
.addHeader("X-DashScope-SSE", "enable")
.build();
log.info("url:{}", openaiUrl);
log.info("requestBody:{}", body);
Response response = chatEngineRetryService.execute(okClient, requestOpenai);
return response;
}
- ExecutorServiceConfig.java
线程池配置类
import com.alibaba.ttl.threadpool.TtlExecutors;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 业务配置。
*/
@Configuration
@Data
public class ExecutorServiceConfig {
/** 业务线程池核心线程数 */
@Value("${chat.remote.request.pool.corePoolSize:25}")
private int businessCorePoolSize;
/** 业务线程池最大线程数 */
@Value("${chat.remote.request.pool.maxPoolSize:50}")
private int businessMaxPoolSize;
/** 业务线程池最大空闲秒数 */
@Value("${chat.remote.request.pool.keepAliveSeconds:60}")
private int businessKeepAliveSeconds;
/** 业务线程池任务队列长度 */
@Value("${chat.remote.request.pool.taskQueueSize:1000}")
private int businessTaskQueueSize;
@Bean(name = "chatRequestExecutor", destroyMethod = "shutdown")
public ExecutorService chatRequestExecutor() {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(businessCorePoolSize, businessMaxPoolSize,
businessKeepAliveSeconds, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(businessTaskQueueSize), new ThreadPoolExecutor.CallerRunsPolicy());
return TtlExecutors.getTtlExecutorService(threadPoolExecutor);
}
}
- ChatEngineRetryService.java、ChatEngineRetryServiceImpl.java
OkClient调用封装接口
// 接口
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public interface ChatEngineRetryService {
Response execute(OkHttpClient client, Request requestOpenai) throws RuntimeException;
}
OkClient调用封装实现类
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.stereotype.Service;
/**
* 可增加重试机制
*/
@Slf4j
@Service
public class ChatEngineRetryServiceImpl implements ChatEngineRetryService {
/**
* @param: client
* @param: requestOpenai
* @description: 封装okClient
* @return: okhttp3.Response
* @author: niaonao
* @date: 2025/6/25
*/
@Override
public Response execute(OkHttpClient client, Request requestOpenai) throws RuntimeException{
try {
Response response = client.newCall(requestOpenai).execute();
log.info("engine res:{}", response.toString());
if (null != response && response.code() != 200) {
throw new RuntimeException("大模型引擎失败,错误码不是200");
}
return response;
}catch (Exception e){
log.error("调用引擎异常", e);
throw new RuntimeException("大模型引擎失败");
}
}
}
- ChatInput.java
大模型请求体封装类
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class ChatInput<T> implements Serializable {
private List<ChatMessage<T>> messages;
private Boolean stream;
private String system;
private Double temperature;
private String model;
private String stop;
private Integer max_tokens;
private ChatResponseFormat response_format;
/** 联网搜索 */
private Boolean enable_search;
/** 深度思考-阿里云百炼 */
private Boolean enable_thinking;
/** 音频大模型-阿里云百炼 */
private ChatInputMessage input;
/** 音频大模型-流式输出-阿里云百炼 */
private ChatParameters parameters;
}
- ChatMessage.java
大模型请求体message封装类
import lombok.Data;
import java.io.Serializable;
@Data
public class ChatMessage<T> implements Serializable {
private String role;
private T content;
}
- ChatInputMessage.java
音频大模型请求体messages封装
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class ChatInputMessage<T> implements Serializable {
private List<ChatMessage<T>> messages;
}
- ChatParameters.java
音频大模型开启流式接口的属性
import lombok.Data;
import java.io.Serializable;
@Data
public class ChatParameters implements Serializable {
private Boolean incremental_output;
}
- 调用音频大模型方法
@Value("${aliyuncs.audioModel:qwen-audio-turbo-latest}")
private String audioModel;
@Value("${aliyuncs.audioUrl:https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation}")
private String apiAudioUrl;
@Value("${aliyuncs.key:sk-xxx}")
private String apiKey;
/**
* @param: videoContentDTO
* @param: emitter
* @description: 视频文案提取,传入的是三方平台视频链接,提取视频文案
* @return: void 流式接口,通过SSE和前端交互
* @author: niaonao
* @date: 2025/6/24
*/
@Override
public void videoLinkAnalysis(VideoContentDTO videoContentDTO, SseEmitter emitter) {
// ...
// 此处获取到音频链接
String musicUrl = dyDetailVO.getAweme_detail().getMusic().getPlay_url().getUri();
String userPrompt = "请提取音频文案";
Map<String, String> contentAudio = new HashMap<>(1);
contentAudio.put("audio".intern(), musicUrl);
Map<String, String> contentText = new HashMap<>(1);
contentText.put("text".intern(), userPrompt);
List<Map<String, String>> contentList = new ArrayList<>(2);
contentList.add(contentAudio);
contentList.add(contentText);
ChatMessage<List<Map<String, String>>> chatMessage = new ChatMessage<List<Map<String, String>>>();
chatMessage.setRole(PromptConstants.ROLE_USER);
chatMessage.setContent(contentList);
List<ChatMessage<List<Map<String, String>>>> messages = new ArrayList<>(1);
messages.add(chatMessage);
ChatInputMessage chatInputMessage = new ChatInputMessage();
chatInputMessage.setMessages(messages);
ChatInput<List<Map<String, String>>> chatInput = new ChatInput<List<Map<String, String>>>();
chatInput.setInput(chatInputMessage);
chatInput.setModel(audioModel);
ChatParameters parameters = new ChatParameters();
parameters.setIncremental_output(true);
chatInput.setParameters(parameters);
openAIApiService.streamIncrementalApi(chatInput, emitter, apiAudioUrl, apiKey);
}
参考文档
大模型服务平台百炼-视觉理解
大模型服务平台百炼-音频理解
三方平台开放平台-通过VideoID获取IFrame代码
Powered By niaonao