文章目录
- 1、前言
- 2、需求说明
- 2.1 需求说明
- 2.2 数据准备
- 3、功能实现
- 3.1 使用视频理解大模型能力
- 3.1.1 三方平台视频在线链接解析
- 3.1.2 三方平台视频内网链接解析
- 3.1.3 三方平台视频转存本地服务
- 3.2 使用音频识别大模型能力
- 3.2.1 三方平台视频在线链接解析
- 3.2.2 三方平台视频详情接口说明
- 3.2.3 通过三方平台视频详情获取音频链接
- 3.2.4 通过音频链接获取视频文案
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代码
https://developer.open-douyin.com/docs/resource/zh-CN/dop/develop/openapi/video-management/douyin/iframe-player/get-iframe-by-video
请求示例:
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;@Datapublic static class LogPb {private String impr_id;}@Datapublic 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;@Datapublic static class Music {/** 音乐名称 */private PlayUrl play_url;@Datapublic static class PlayUrl {/** 音频地址 */private String uri;}}@Datapublic static class SeoInfo {/** ocr_content */private String ocr_content;}@Datapublic static class TextExtra {/** 标签名称 */private String hashtag_name;}@Datapublic static class Video {/** 格式 */private String format;/** 分辨率 */private String ratio;private PlayAddr play_addr;@Datapublic static class PlayAddr {/*** 播放地址,取list.get(0)* 对帧率有要求可以取其他值* play_addr* play_addr_265* play_addr_h264*/private List<String> url_list;}}@Datapublic 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*/@Overridepublic 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