
Byted Mediakit Process Tools
- 12 installs
- 411 repo stars
- Updated August 4, 2026
- bytedance/agentkit-samples
byted-mediakit-process-tools is a Claude skill wrapping Volcengine AI MediaKit for video understanding, editing and enhancement, with automatic fallback to local FFmpeg.
About
This skill is a Volcengine AI MediaKit audio/video toolset that an agent invokes for trimming, concatenation, audio extraction, quality enhancement, image-to-video, video understanding and local edits like flip, speed, subtitle, watermark and transcode. A developer uses it to run media operations that auto-route to AMK cloud when configured with URL inputs, or fall back to local FFmpeg. Cloud features need an AMK_API_KEY, and video understanding needs an ARK_API_KEY and model id.
- Volcengine AI MediaKit toolset for video understanding, editing, enhancement and text-to-video
- Auto-selects backend: AMK cloud for URL inputs when configured, local FFmpeg otherwise
- Local-native commands (flip, speed, subtitle, watermark, transcode) always run through FFmpeg
Byted Mediakit Process Tools by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,038 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
byted-mediakit-process-tools capabilities & compatibility
Cloud features need an AMK_API_KEY (and ARK key for understanding), billed by Volcengine; local FFmpeg operations run free without keys.
- Capabilities
- video editing · video understanding · quality enhancement · transcoding
- Use cases
- video generation · transcription
- Platforms
- macOS · Linux
- Pricing
- Bring your own API key
What byted-mediakit-process-tools says it does
音视频裁剪、拼接、音频提取、音视频合成会自动选择执行后端:云端环境完整且输入为 URL 时走 AMK 云端;云端必需配置/依赖缺失,或输入是本地文件路径时,自动走本地 FFmpeg。
`禁止修改任何返回数据信息`,如 `play_url` 、`request_id` 、`task_id` 等
npx skills add https://github.com/bytedance/agentkit-samples --skill byted-mediakit-process-toolsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 411 |
| Last updated | August 4, 2026 |
| Repository | bytedance/agentkit-samples ↗ |
What it does
Trim, concat, enhance and understand video with Volcengine AI MediaKit, falling back to local FFmpeg.
Who is it for?
Trimming, concatenating, extracting audio, enhancing quality, image-to-video, and local flip/speed/subtitle/watermark/transcode edits.
Skip if: Users needing cloud enhancement or text-to-video with no AMK_API_KEY, since those cloud-only features cannot fall back to FFmpeg.
When should I use this skill?
The user wants to edit, enhance, transcode or understand video, or run local FFmpeg operations like flip, speed change or watermarking.
What you get
Runs the requested media operation on AMK cloud or local FFmpeg and returns a download link, request id and task id.
- processed media download link
- request_id and task_id
By the numbers
- 12-row capability table (cloud, cloud-and-local, local)
- one sync and six async capabilities plus local commands
Files
说明:宿主若在环境中注入ARK_SKILL_API_BASE/ARK_SKILL_API_KEY(例如供其他 Skill 走 SkillHub 网关),与本 Skill 的AMK_API_KEY、ARK_API_KEY(视频理解)相互独立,请勿混淆。
⚠️ 严格执行:音视频裁剪、拼接、音频提取、音视频合成会自动选择执行后端:云端环境完整且输入为 URL 时走 AMK 云端;云端必需配置/依赖缺失,或输入是本地文件路径时,自动走本地 FFmpeg。视频翻转、调速、加字幕、加水印、转码是本地原生命令,始终走本地 FFmpeg。不要为了这些本地能力向用户索取云端环境变量。
<SKILL_DIR>为byted-mediakit-process-tools所在目录。
当前方法返回的 链接仅供下载,不支持播放能力禁止修改任何返回数据信息,如play_url、request_id、task_id等
用户明确声明需要重新执行时:除 understand_video_content 外的方法需 生成新的 `client_token`(不要复用上一次的 `client_token`),避免命中上次的幂等结果火山引擎 AI MediaKit 音视频处理工具集
概览
本工具集支持以下音视频处理能力。标记为“云端&本地”的能力会自动选择执行后端:云端配置完整且输入为 URL 时走 AMK 云端;云端不可用或输入为本地路径时走本地 FFmpeg。
| 能力 | 支持范围 | 说明 |
|---|---|---|
| 视频理解 | 云端 | AI 分析视频内容,生成自然语言描述 |
| 音视频裁剪 | 云端&本地 | 精确裁剪音频或视频时长 |
| 音视频拼接 | 云端&本地 | 拼接多个音频或视频片段;云端支持转场 ID,本地支持 FFmpeg 转场名 |
| 音频提取 | 云端&本地 | 从视频中提取音频轨道 |
| 音视频合成 | 云端&本地 | 合成或替换视频音轨;云端支持时长对齐策略,本地支持混音/替换 |
| 画质增强 | 云端 | 提升视频画质、分辨率、帧率 |
| 文生视频 | 云端 | 图片生成视频,支持动画和转场 |
| 视频翻转 | 本地 | 水平、垂直或双向翻转 |
| 视频调速 | 本地 | 调整视频与音频播放速度 |
| 视频加字幕 | 本地 | 添加 SRT/ASS 硬字幕 |
| 视频加水印 | 本地 | 添加图片/水印,支持位置、缩放和显示时间 |
| 视频转码/转封装 | 本地 | 转换封装格式或重编码 |
---
获取密钥
如需使用云端能力,请先获取 API 密钥;本地可覆盖能力在密钥缺失时会自动使用 FFmpeg:
- AI MediaKit 控制台:https://console.volcengine.com/imp/ai-mediakit/
- 方舟模型与密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seed-1-8
---
快速开始
1. 环境配置
在 <SKILL_DIR>/.env 中配置环境变量(首次使用会自动创建模板):
# AMK API Key(云端能力需要;本地回退不需要)
AMK_API_KEY=your_amk_api_key_here
# AMK 环境固定为 prod
AMK_ENV=prod
# 是否启用 client_token 自动注入(用于幂等)
AMK_ENABLE_CLIENT_TOKEN=false
# 方舟 密钥(可选,仅使用视频理解功能时必须配置)
ARK_API_KEY=your_ark_api_key_here
# 方舟 模型ID(可选,仅使用视频理解功能时必须配置)
ARK_MODEL_ID=doubao-seed-1-82. 依赖安装
cd <SKILL_DIR>/scripts
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
python -m pip install -r requirements.txt---
核心功能
同步能力(立即返回结果)
| 能力 | 说明 |
|---|---|
| understand_video_content | 视频内容理解,使用 AI 分析视频并生成自然语言描述 |
异步能力(默认自动等待结果)
| 能力 | 说明 |
|---|---|
| trim_media_duration | 裁剪音视频时长,精确到毫秒 |
| concat_media_segments | 拼接多个音视频片段,支持转场效果 |
| extract_audio | 从视频中提取音频轨道,支持 mp3/m4a 格式 |
| enhance_video | 视频画质增强,支持超分、插帧等 |
| image_to_video | 图片生成视频,支持动画和转场 |
| mux_audio_video | 音视频合成,支持时长对齐 |
辅助能力
| 能力 | 说明 |
|---|---|
| query_task | 查询异步任务执行状态和结果 |
本地原生命令
| 能力 | 说明 |
|---|---|
| flip-video / flip_video | 视频翻转,默认水平镜像 |
| adjust-speed / adjust_speed | 视频调速 |
| add-subtitle / add_subtitle | 视频加硬字幕,支持 SRT/ASS |
| add-overlay / add_overlay | 视频加图片/水印 |
| transcode | 视频转码/转封装 |
---
使用示例
视频理解
./byted-mediakit-process-tools.sh understand_video_content \
--video_url "https://example.com/video.mp4" \
--prompt "总结视频内容" \
--fps 1视频裁剪
# 云端:输入为 URL 且 AMK 配置完整
./byted-mediakit-process-tools.sh trim_media_duration \
--type video \
--source "https://example.com/video.mp4" \
--start_time 0 \
--end_time 10
# 本地回退:输入为本地文件,或 AMK 配置不完整
./byted-mediakit-process-tools.sh trim_media_duration \
--type video \
--source "/path/to/input.mp4" \
--start_time 0 \
--end_time 10 \
--output "/path/to/output.mp4"音视频拼接
./byted-mediakit-process-tools.sh concat_media_segments \
--type video \
--sources "https://example.com/1.mp4" "https://example.com/2.mp4"音频提取
./byted-mediakit-process-tools.sh extract_audio \
--video_url "https://example.com/video.mp4" \
--format mp3画质增强
./byted-mediakit-process-tools.sh enhance_video \
--video_url "https://example.com/video.mp4" \
--tool_version professional \
--resolution 1080p图片生成视频
./byted-mediakit-process-tools.sh image_to_video \
--images "image_url=https://example.com/1.jpg,duration=3,animation_type=zoom_in" \
"image_url=https://example.com/2.jpg,duration=3,animation_type=pan_left"音视频合成
./byted-mediakit-process-tools.sh mux_audio_video \
--video_url "https://example.com/video.mp4" \
--audio_url "https://example.com/audio.mp3" \
--is_audio_reserve false本地翻转 / 调速 / 字幕 / 水印 / 转码
./byted-mediakit-process-tools.sh flip-video -i input.mp4 --direction horizontal -o flipped.mp4
./byted-mediakit-process-tools.sh adjust-speed -i input.mp4 --speed 2.0 -o speed.mp4
./byted-mediakit-process-tools.sh add-subtitle -i input.mp4 --subtitle sub.srt -o subtitled.mp4
./byted-mediakit-process-tools.sh add-overlay -i input.mp4 --image logo.png --position top-right -o watermarked.mp4
./byted-mediakit-process-tools.sh transcode -i input.mov --format mp4 --codec h264 -o output.mp4异步任务(不等待结果)
# 使用 --no-wait 立即返回 task_id
./byted-mediakit-process-tools.sh --no-wait trim_media_duration \
--type video \
--source "https://example.com/video.mp4" \
--start_time 0 \
--end_time 10
# 查询任务结果
./byted-mediakit-process-tools.sh query_task --task_id "amk-xxx-xxx"---
响应格式
同步响应(视频理解)
{
"status": "success",
"result": {
"choices": [
{
"role": "assistant",
"content": "视频内容分析结果..."
}
]
}
}异步响应(默认自动等待)
{
"task_id": "amk-tool-extract-audio-xxxxxxxxxxxxxx",
"duration": 82.454056,
"play_url": "https://example.vod.cn-north-1.volcvideo.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.mp3?preview=1&auth_key=***",
"request_id": "20260401xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "completed",
"task_type": "extract-audio"
}异步响应(--no-wait)
{
"status": "pending",
"task_id": "amk-xxx-xxx",
"message": "任务已提交,已跳过等待,可调用 query_task 接口传入 task_id 查询结果",
"query_example": "./byted-mediakit-process-tools.sh query_task --task_id amk-xxx-xxx"
}错误响应
{
"status": "failed/canceled/timeout",
"task_id": "amk-xxx-xxx",
"message": "错误详情"
}---
详细文档
各功能的详细参数说明请参考 reference/ 目录下的对应文档:
| 能力 | 文档链接 |
|---|---|
| understand_video_content | reference/understand_video_content.md |
| query_task | reference/query_task.md |
| concat_media_segments | reference/concat_media_segments.md |
| enhance_video | reference/enhance_video.md |
| extract_audio | reference/extract_audio.md |
| image_to_video | reference/image_to_video.md |
| mux_audio_video | reference/mux_audio_video.md |
| trim_media_duration | reference/trim_media_duration.md |
| 统一响应格式 | reference/common_response.md |
| 本地回退策略 | reference/local_fallback.md |
---
注意事项
1. 返回链接:所有返回的 play_url 等链接仅供下载,不支持直接播放 2. 幂等性:重新执行任务时,请确保生成新的 client_token(AMK_ENABLE_CLIENT_TOKEN=true 时自动处理) 3. 视频理解:使用视频理解功能必须配置 ARK_API_KEY 和 ARK_MODEL_ID 4. 超时处理:大文件处理可能耗时较长,建议使用 --no-wait 配合 query_task 轮询 5. 本地能力:trim_media_duration、concat_media_segments、extract_audio、mux_audio_video 支持本地 FFmpeg 回退;flip-video、adjust-speed、add-subtitle、add-overlay、transcode 始终走本地 FFmpeg;enhance_video、image_to_video、understand_video_content、query_task 仅走云端
---
© 北京火山引擎科技有限公司 2026 版权所有
#!/bin/bash
# AMK音视频处理工具集启动脚本
# 兼容各种执行路径
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
cd "$SCRIPT_DIR/scripts" || exit 1
if [ -f ".venv/bin/activate" ]; then
source .venv/bin/activate
fi
if command -v python3 >/dev/null 2>&1; then
python3 main.py "$@"
else
python main.py "$@"
fi
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
统一返回格式
除 understand_video_content(视频理解)外,异步能力的 HTTP 结果与 CLI 包装的 status / task_id 等字段约定如下。
视频理解(同步,直接返回模型结果)
{
"status": "success",
"result": {
"choices": [
{
"role": "assistant",
"content": "视频内容分析结果..."
}
]
}
}异步能力 — 成功(默认自动等待结果)
{
"task_id": "amk-tool-extract-audio-xxxxxxxxxxxxxx",
"duration": 82.454056,
"play_url": "https://example.vod.cn-north-1.volcvideo.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.mp3?preview=1&auth_key=***",
"request_id": "20260401xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"status": "completed",
"task_type": "extract-audio"
}异步能力 — --no-wait(仅返回 task_id)
{
"status": "pending",
"task_id": "amk-xxx-xxx",
"message": "任务已提交,已跳过等待,可调用 query_task 接口传入 task_id 查询结果",
"query_example": "./byted-mediakit-process-tools.sh query_task --task_id amk-xxx-xxx"
}错误 / 超时
{
"status": "failed/canceled/timeout",
"task_id": "amk-xxx-xxx",
"message": "错误详情"
}音、视频拼接能力
功能命名
concat_media_segments
作用
- 对音频或视频进行拼接,视频支持转场效果。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | string | ✅ | 拼接类型:audio \ |
| sources | array | ✅ | 待拼接资源列表。URL 且云端配置完整时走云端;本地路径或云端配置缺失时走本地 FFmpeg,文件数量不超过 100个 |
| transitions | array | ❌ | 云端视频转场 ID 列表;音频拼接不支持 |
| output | string | ❌ | 仅本地回退生效,指定输出文件路径 |
| local_transition | string | ❌ | 仅本地回退生效,FFmpeg xfade 转场名,如 fade / circleopen / radial |
| local_transition_duration | float | ❌ | 仅本地回退生效,本地转场时长,默认 1 秒 |
可选转场 ID(仅视频)
| 分类 | ID |
|---|---|
| 交替出场 | 1182359 |
| 旋转放大 | 1182360 |
| 泛开 | 1182358 |
| 六角形 | 1182365 |
| 故障转换 | 1182367 |
| 飞眼 | 1182368 |
| 梦幻放大 | 1182369 |
| 开门展现 | 1182370 |
| 立方转换 | 1182373 |
| 透镜变换 | 1182374 |
| 晚霞转场 | 1182375 |
| 圆形交替 | 1182378 |
- 若不提供转场,默认无转场;当拼接点多于转场数量时,系统会循环使用转场。
- 本地回退不映射云端转场 ID;如需本地转场,请使用
local_transition。
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
画质增强能力
功能命名
enhance_video
作用
- 视频画质增强,支持最高到
4k目标分辨率。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_url | string | ✅ | 输入视频 URL,支持 http:// 或 https:// |
| tool_version | string | ❌ | 工具版本:standard(标准版,默认)\ |
| resolution | string | ❌ | 目标分辨率:240p \ |
| resolution_limit | int | ❌ | 目标长宽限制,范围 [64, 2160] |
| fps | float | ❌ | 目标帧率,范围 (0, 120] |
分辨率选择策略(宿主agent)
- 若用户超过
20s, 默认选择原始分辨率 - 若用户明确表示“使用原始分辨率/保持原画”,则调用时不传
resolution。 - 若用户明确指定目标分辨率(如
720p、2k、4k),则传resolution。 - 若用户未提及分辨率,宿主agent在调用前需要追问:
需要使用原始分辨率还是选择目标分辨率?;根据回答决定是否传resolution。
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
音频提取能力
功能命名
extract_audio
作用
- 从视频中提取音频,支持输出
mp3或m4a, 默认输出格式为m4a 。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_url | string | ✅ | 输入视频。URL 且云端配置完整时走云端;本地路径或云端配置缺失时走本地 FFmpeg |
| format | string | ❌ | 输出格式:mp3 \ |
| output | string | ❌ | 仅本地回退生效,指定输出文件路径 |
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
图片转视频能力
功能命名
image_to_video
作用
- 将图片序列合成为视频,支持单图动画及转场效果。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| images | array | ✅ | 待合成图片列表;CLI 可用 image_url=xxx,duration=3,animation_type=zoom_in,多图空格分隔 |
| transitions | array | ❌ | 视频转场 ID 列表,同 `concat_media_segments.md` 中的转场 ID 表 |
images 单项字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| image_url | string | ✅ | 图片 URL |
| duration | float | ❌ | 播放时长,默认 3 秒,最多 2 位小数 |
| animation_type | string | ❌ |
图片的动画类型,选填,不填时无动画效果。 move_up:向上移动 move_down:向下移动 move_left:向左移动 move_right:向右移动 zoom_in:缩小 zoom_out:放大|
| animation_in | float | ❌ | 动画开始时间,支持2位小数。默认为图片展示时长,表示动画随图片播放同时结束,单位:秒" | | animation_out | float | ❌ | "动画结束时间,,支持2位小数。默认为图片展示时长,表示动画随图片播放同时结束,单位:秒" |
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
Local FFmpeg Fallback
byted-mediakit-process-tools includes scripts/local_ffmpeg_tool.py for local processing when cloud execution is unavailable or unsuitable.
Routing Rules
The CLI chooses the backend automatically:
- Use AMK cloud when required cloud configuration and dependencies are complete and the command input is an
http://orhttps://URL. - Use local FFmpeg when a supported command receives a local file path.
- Use local FFmpeg when a supported command cannot use cloud execution because required cloud configuration or Python dependencies are missing.
- Keep cloud-only capabilities on AMK:
enhance_video,image_to_video,understand_video_content, andquery_task. - Always use local FFmpeg for local-native commands:
flip-video,adjust-speed,add-subtitle,add-overlay, andtranscode.
Do not ask users to provide cloud environment variables for local-supported commands. The fallback path is intended to be invisible except for the JSON response fields such as backend: "local_ffmpeg" and a generic fallback reason.
Local-Supported Commands
| AMK command | Local command | Notes |
|---|---|---|
trim_media_duration --type video | trim-video | Supports --start_time, --end_time, and optional --output. |
trim_media_duration --type audio | trim-audio | Supports --start_time, --end_time, and optional --output. |
concat_media_segments --type video | concat-video | Supports local/URL inputs. Cloud transition IDs are not mapped; use --local_transition for local xfade names. |
concat_media_segments --type audio | concat-audio | Supports local/URL inputs and optional --output. |
extract_audio | extract-audio | Supports mp3 and m4a through the public wrapper; local tool can handle more formats directly. |
mux_audio_video | mux-audio-video | is_audio_reserve=true maps to local mix mode; false maps to local replace mode. Local fallback does not support duration sync options. |
flip-video / flip_video | flip-video | Local-only. Defaults to horizontal flip; supports horizontal, vertical, and both. |
adjust-speed / adjust_speed | adjust-speed | Local-only. Uses FFmpeg setpts and atempo. |
add-subtitle / add_subtitle | add-subtitle | Local-only hard subtitles for SRT/ASS. |
add-overlay / add_overlay | add-overlay | Local-only image/watermark overlay with position, scale, and optional time range. |
transcode | transcode | Local-only transcode/remux. Defaults to stream copy unless a codec is specified. |
Examples
./byted-mediakit-process-tools.sh trim_media_duration \
--type video \
--source ./input.mp4 \
--start_time 1 \
--end_time 5 \
--output ./trimmed.mp4./byted-mediakit-process-tools.sh concat_media_segments \
--type video \
--sources ./a.mp4 ./b.mp4 \
--output ./concat.mp4./byted-mediakit-process-tools.sh mux_audio_video \
--video_url ./video.mp4 \
--audio_url ./audio.mp3 \
--is_audio_reserve false \
--output ./replaced.mp4./byted-mediakit-process-tools.sh flip-video \
-i ./input.mp4 \
--direction horizontal \
-o ./flipped.mp4./byted-mediakit-process-tools.sh transcode \
-i ./input.mov \
--format mp4 \
--codec h264 \
-o ./output.mp4视频加音频能力
功能命名
mux_audio_video
作用
- 将视频和音频进行合成,支持保留原音轨及音视频时长对齐策略。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_url | string | ✅ | 待处理视频。URL 且云端配置完整时走云端;本地路径或云端配置缺失时走本地 FFmpeg |
| audio_url | string | ✅ | 待处理音频。URL 且云端配置完整时走云端;本地路径或云端配置缺失时走本地 FFmpeg |
| is_audio_reserve | bool | ❌ | 是否保留原视频音频,默认 true |
| is_video_audio_sync | bool | ❌ | 是否对齐音视频时长,默认 false |
| sync_mode | string | ❌ | 对齐模式:video \ |
| sync_method | string | ❌ | 对齐方式:speed \ |
| output | string | ❌ | 仅本地回退生效,指定输出文件路径 |
本地回退时,is_audio_reserve=true 会混合原音频和新音频;false 会替换为新音频。本地回退暂不支持 is_video_audio_sync=true 的时长对齐。
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
任务查询能力
功能命名
query_task
作用
- 查询异步任务状态,支持轮询直到任务完成。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| task_id | string | ✅ | 任务查询 ID |
| interval | int | ❌ | 轮询间隔,默认 5 秒 |
| max_retries | int | ❌ | 最大轮询次数,默认 6 次 |
返回数据
- duration(float): 非必选,时长(秒)
- play_url(str): 非必选,播放地址(根据任务类型解析为音频或视频地址)
- request_id(str): 日志 id
- status(str): 任务状态(
running\|completed\|queued\|failed\|canceled) - task_id(str): 任务查询 ID
音、视频剪切能力
功能命名
trim_media_duration
作用
- 按时间范围裁剪音频或视频,
end_time必须大于start_time。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| type | string | ✅ | 媒体类型:audio \ |
| source | string | ✅ | 待剪切资源。URL 且云端配置完整时走云端;本地路径或云端配置缺失时走本地 FFmpeg |
| start_time | float | ❌ | 裁剪开始时间,默认 0,最多 2 位小数,单位秒 |
| end_time | float | ❌ | 裁剪结束时间,默认片源结尾,最多 2 位小数,单位秒 |
| output | string | ❌ | 仅本地回退生效,指定输出文件路径 |
返回数据
- task_id(str): 任务查询 id
- request_id(str): 日志 id
视频理解能力
功能命名
understand_video_content
作用
- 使用大模型进行视频理解,生成视频内容解析与自然语言描述。
参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| video_url | string | ✅ | 视频文件 URL,支持 http:// 或 https:// |
| prompt | string | ✅ | 用户提示词,描述对视频理解的具体要求 |
| fps | float | ✅ | 抽帧帧率,须大于 0,支持整数或浮点数 |
| max_frames | int | ❌ | 最大抽帧数,须大于 0;不传则由服务端默认策略处理 |
环境变量
- ARK_MODEL_ID(str): 必选,方舟模型 ID,用于请求体
model字段 - 未设置、为空或
null时会抛出配置异常
返回数据
- choices(List[message]): 仅返回模型
choices中的message列表 - message.role(str): 角色(通常为
assistant) - message.content(str): 主回答内容
- message.reasoning_content(str): 推理内容(模型支持时返回)
说明
- 方法会在内部自动构造
chat.completions请求体,不需要外部传递messages/model/stream - 内部另有默认上限等行为,与网关/模型版本有关;
max_frames见上表 - 返回不会包含
usage/id/model等元信息,仅保留choices[].message
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""AMK HTTP client: GET/POST/PUT/DELETE, streaming, AMK_ENV host selection."""
from amk_client.api_config import (
PATH_PARAM_MODELS,
ApiEntry,
api_config,
get_api_entry,
path_placeholder_names,
validate_and_format_path,
)
from amk_client.api_path_params import QueryTaskPathParams
from amk_client.api_request import (
ApiRequest,
AsyncApiRequest,
concat_audio,
concat_media_segments,
concat_video,
enhance_video,
extract_audio,
image_to_video,
mux_audio_video,
normalize_query_task_response,
query_task,
query_task_with_polling,
trim_audio,
trim_media_duration,
trim_video,
understand_video_content,
)
from amk_client.auth_mode import AmkAuthMode
from amk_client.models import (
ConcatMediaKind,
ConcatMediaSegmentsAsyncResult,
ConcatMediaSegmentsRequest,
ConcatMediaSegmentsResponse,
EnhanceVideoRequest,
ExtractAudioRequest,
ImageToVideoImageItem,
ImageToVideoRequest,
MuxAudioVideoRequest,
QueryTaskNormalizedResult,
TaskStatusStr,
TransitionId,
TrimMediaDurationAsyncResult,
TrimMediaDurationRequest,
TrimMediaDurationResponse,
TrimMediaKind,
)
from amk_client.client import AmkAsyncClient, AmkClient
from amk_client.config import (
MissingConfigError,
build_authorization_header,
default_ark_api_key,
default_bearer_token,
normalize_secret,
require_amk_api_key,
require_ark_api_key,
require_credentials_for_mode,
resolve_base_url,
resolve_default_auth_mode,
)
__all__ = [
"AmkAuthMode",
"AmkAsyncClient",
"AmkClient",
"ApiEntry",
"AsyncApiRequest",
"ApiRequest",
"ConcatMediaKind",
"ConcatMediaSegmentsAsyncResult",
"concat_audio",
"concat_media_segments",
"concat_video",
"ConcatMediaSegmentsRequest",
"ConcatMediaSegmentsResponse",
"EnhanceVideoRequest",
"enhance_video",
"ExtractAudioRequest",
"ImageToVideoImageItem",
"ImageToVideoRequest",
"MuxAudioVideoRequest",
"mux_audio_video",
"normalize_query_task_response",
"query_task",
"query_task_with_polling",
"QueryTaskNormalizedResult",
"TaskStatusStr",
"MissingConfigError",
"PATH_PARAM_MODELS",
"QueryTaskPathParams",
"TrimMediaDurationAsyncResult",
"TrimMediaDurationRequest",
"TrimMediaDurationResponse",
"trim_audio",
"trim_media_duration",
"trim_video",
"TransitionId",
"understand_video_content",
"image_to_video",
"extract_audio",
"TrimMediaKind",
"api_config",
"build_authorization_header",
"default_ark_api_key",
"default_bearer_token",
"get_api_entry",
"normalize_secret",
"path_placeholder_names",
"require_amk_api_key",
"require_ark_api_key",
"require_credentials_for_mode",
"resolve_base_url",
"resolve_default_auth_mode",
"validate_and_format_path",
]
"""AMK HTTP API 注册表:路径、方法、鉴权模式。"""
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import re
from typing import Any, Literal, Mapping
from pydantic import BaseModel, ConfigDict
from amk_client.api_path_params import QueryTaskPathParams
from amk_client.auth_mode import AmkAuthMode
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]
_PATH_PLACEHOLDERS = re.compile(r"\{(\w+)\}")
class ApiEntry(BaseModel):
"""单条 API 配置(不可变)。"""
model_config = ConfigDict(frozen=True)
action: str
auth_mode: AmkAuthMode
method: HttpMethod = "POST"
# api_name -> 路径参数模型(无路径占位符的 API 不必注册)
PATH_PARAM_MODELS: dict[str, type[BaseModel]] = {
"query_task": QueryTaskPathParams,
}
api_config: dict[str, ApiEntry] = {
"understand_video_content": ApiEntry(
action="/api/v1/chat/completions",
auth_mode=AmkAuthMode.ARK_AND_MEDIAKIT,
method="POST",
),
"trim_audio": ApiEntry(
action="/api/v1/tools/trim-audio",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"trim_video": ApiEntry(
action="/api/v1/tools/trim-video",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
# 调用节点聚合:统一由 concat_media_segments(type=audio|video) 分发到底层 concat_* API
"concat_audio": ApiEntry(
action="/api/v1/tools/concat-audio",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"concat_video": ApiEntry(
action="/api/v1/tools/concat-video",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"mux_audio_video": ApiEntry(
action="/api/v1/tools/mux-audio-video",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"image_to_video": ApiEntry(
action="/api/v1/tools/image-to-video",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"extract_audio": ApiEntry(
action="/api/v1/tools/extract-audio",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"enhance_video": ApiEntry(
action="/api/v1/tools/enhance-video",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="POST",
),
"query_task": ApiEntry(
action="/api/v1/tasks/{task_id}",
auth_mode=AmkAuthMode.MEDIAKIT_ONLY,
method="GET",
),
}
def get_api_entry(name: str) -> ApiEntry:
"""按注册名取配置;未知名称抛 ``KeyError``。"""
return api_config[name]
def path_placeholder_names(action: str) -> tuple[str, ...]:
return tuple(_PATH_PLACEHOLDERS.findall(action))
def validate_and_format_path(
api_name: str,
entry: ApiEntry,
path_params: Mapping[str, Any] | BaseModel | None,
) -> str:
"""用 Pydantic 校验路径参数(若已注册模型),再 ``str.format`` 生成最终 path。"""
names = path_placeholder_names(entry.action)
if not names:
if path_params:
raise ValueError(
f"API {api_name!r} 的 action 无路径占位符,不应传入 path_params: {path_params!r}"
)
return entry.action
raw: Mapping[str, Any]
if path_params is None:
raw = {}
elif isinstance(path_params, BaseModel):
raw = path_params.model_dump()
else:
raw = path_params
model_cls = PATH_PARAM_MODELS.get(api_name)
if model_cls is not None:
data = model_cls.model_validate(raw).model_dump()
else:
data = dict(raw)
missing = [n for n in names if n not in data]
if missing:
raise ValueError(
f"API {api_name!r} 缺少路径参数: {missing};已注册模型时可自动校验类型与约束"
)
return entry.action.format(**{n: str(data[n]) for n in names})
"""各 API 路径占位符对应的 Pydantic 校验模型(与 ``api_config`` 中 ``action`` 的 ``{name}`` 一致)。"""
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
class QueryTaskPathParams(BaseModel):
"""``GET /api/v1/tasks/{task_id}``"""
model_config = ConfigDict(str_strip_whitespace=True)
task_id: str = Field(..., min_length=1, description="任务 ID")
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""按 ``api_config`` 发起请求;路径参数经 Pydantic 校验。"""
from __future__ import annotations
import asyncio
import json
import os
import uuid
from typing import Any, Mapping
import httpx
from pydantic import BaseModel
from amk_client.api_config import get_api_entry, validate_and_format_path
from amk_client.client import AmkAsyncClient, AmkClient
from amk_client.config import MissingConfigError, normalize_secret
from amk_client.models import (
ConcatMediaSegmentsAsyncResult,
ConcatMediaSegmentsRequest,
ConcatMediaSegmentsResponse,
EnhanceVideoRequest,
ExtractAudioRequest,
ImageToVideoRequest,
MuxAudioVideoRequest,
QueryTaskNormalizedResult,
TaskStatusStr,
TrimMediaDurationAsyncResult,
TrimMediaDurationRequest,
TrimMediaDurationResponse,
)
class ApiRequest:
"""封装单个注册 API:路径参数校验 + 按配置的 method/auth_mode 调用 ``AmkClient``。"""
def __init__(self, api_name: str, *, client: AmkClient | None = None) -> None:
self.api_name = api_name
self.entry = get_api_entry(api_name)
self.client = client if client is not None else AmkClient()
def build_path(
self,
path_params: Mapping[str, Any] | BaseModel | None = None,
) -> str:
return validate_and_format_path(self.api_name, self.entry, path_params)
def request(
self,
*,
path_params: Mapping[str, Any] | BaseModel | None = None,
**kwargs: Any,
) -> httpx.Response:
path = self.build_path(path_params)
method = self.entry.method.upper()
auth_mode = self.entry.auth_mode
force_new_client_token = bool(kwargs.pop("force_new_client_token", False))
kwargs = _inject_client_token_if_needed(
self.api_name, kwargs, force_new_client_token=force_new_client_token
)
if method in ("GET", "POST", "PUT", "DELETE"):
return getattr(self.client, method.lower())(
path, auth_mode=auth_mode, **kwargs
)
return self.client.request(method, path, auth_mode=auth_mode, **kwargs)
class AsyncApiRequest:
"""异步版 API 调用封装。"""
def __init__(self, api_name: str, *, client: AmkAsyncClient | None = None) -> None:
self.api_name = api_name
self.entry = get_api_entry(api_name)
self.client = client if client is not None else AmkAsyncClient()
def build_path(
self,
path_params: Mapping[str, Any] | BaseModel | None = None,
) -> str:
return validate_and_format_path(self.api_name, self.entry, path_params)
async def request(
self,
*,
path_params: Mapping[str, Any] | BaseModel | None = None,
**kwargs: Any,
) -> httpx.Response:
path = self.build_path(path_params)
method = self.entry.method.upper()
auth_mode = self.entry.auth_mode
force_new_client_token = bool(kwargs.pop("force_new_client_token", False))
kwargs = _inject_client_token_if_needed(
self.api_name, kwargs, force_new_client_token=force_new_client_token
)
if method in ("GET", "POST", "PUT", "DELETE"):
return await getattr(self.client, method.lower())(
path, auth_mode=auth_mode, **kwargs
)
return await self.client.request(method, path, auth_mode=auth_mode, **kwargs)
def _inject_client_token_if_needed(
api_name: str,
kwargs: Mapping[str, Any],
*,
force_new_client_token: bool = False,
) -> dict[str, Any]:
"""为非视频理解请求按开关注入幂等 token。"""
patched = dict(kwargs)
if api_name == "understand_video_content":
return patched
enabled = _is_client_token_enabled()
if not enabled and not force_new_client_token:
return patched
payload = patched.get("json")
if not isinstance(payload, Mapping):
return patched
body = dict(payload)
if force_new_client_token or "client_token" not in body:
body["client_token"] = uuid.uuid4().hex[:8]
patched["json"] = body
return patched
def _is_client_token_enabled() -> bool:
return (os.getenv("AMK_ENABLE_CLIENT_TOKEN") or "false").strip().lower() == "true"
async def trim_audio(
payload: TrimMediaDurationRequest,
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = {"audio_url": payload.source, "start_time": payload.start_time}
if payload.end_time is not None:
body["end_time"] = payload.end_time
return await AsyncApiRequest("trim_audio", client=use_client).request(json=body)
async def trim_video(
payload: TrimMediaDurationRequest,
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = {"video_url": payload.source, "start_time": payload.start_time}
if payload.end_time is not None:
body["end_time"] = payload.end_time
return await AsyncApiRequest("trim_video", client=use_client).request(json=body)
async def trim_media_duration(
payload: TrimMediaDurationRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> TrimMediaDurationAsyncResult:
"""聚合方法:按 type 分发 trim_audio/trim_video,异常时返回 error。"""
try:
body = (
payload
if isinstance(payload, TrimMediaDurationRequest)
else TrimMediaDurationRequest.model_validate(payload)
)
if body.type == "audio":
res = await trim_audio(body, client=client)
else:
res = await trim_video(body, client=client)
res.raise_for_status()
data = TrimMediaDurationResponse.model_validate(res.json())
return TrimMediaDurationAsyncResult(
task_id=data.task_id, request_id=data.request_id
)
except Exception as exc: # noqa: BLE001 - 统一返回 error 给调用方
return TrimMediaDurationAsyncResult(error=str(exc))
async def concat_audio(
payload: ConcatMediaSegmentsRequest,
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = {"audio_urls": payload.sources}
return await AsyncApiRequest("concat_audio", client=use_client).request(json=body)
async def concat_video(
payload: ConcatMediaSegmentsRequest,
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = {"video_urls": payload.sources}
if payload.transitions is not None:
body["transitions"] = payload.transitions
return await AsyncApiRequest("concat_video", client=use_client).request(json=body)
async def concat_media_segments(
payload: ConcatMediaSegmentsRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> ConcatMediaSegmentsAsyncResult:
"""聚合方法:按 type 分发 concat_audio/concat_video,异常时返回 error。"""
try:
body = (
payload
if isinstance(payload, ConcatMediaSegmentsRequest)
else ConcatMediaSegmentsRequest.model_validate(payload)
)
if body.type == "audio":
res = await concat_audio(body, client=client)
else:
res = await concat_video(body, client=client)
res.raise_for_status()
data = ConcatMediaSegmentsResponse.model_validate(res.json())
return ConcatMediaSegmentsAsyncResult(
task_id=data.task_id, request_id=data.request_id
)
except Exception as exc: # noqa: BLE001 - 统一返回 error 给调用方
return ConcatMediaSegmentsAsyncResult(error=str(exc))
async def understand_video_content(
prompt: str,
video_url: str,
fps: int | float,
max_frames: int | None = None,
*,
client: AmkAsyncClient | None = None,
) -> list[dict[str, Any]]:
"""
视频理解(固定入参版本):
- 接收 prompt、video_url、fps,支持可选 max_frames
- 内部组装 chat.completions payload
- 仅返回 choices[].message
"""
if not prompt.strip():
raise ValueError("prompt 不能为空")
if not (video_url.startswith("http://") or video_url.startswith("https://")):
raise ValueError("video_url 须为 http:// 或 https:// 开头")
if fps <= 0:
raise ValueError("fps 必须大于 0")
if max_frames is not None and max_frames <= 0:
raise ValueError("max_frames 必须大于 0")
model_id = normalize_secret(os.environ.get("ARK_MODEL_ID"))
if model_id is None:
raise MissingConfigError.missing_env(
"ARK_MODEL_ID",
purpose="视频理解 chat.completions 的 model 字段。",
value_placeholder="<Ark Model ID>",
)
use_client = client if client is not None else AmkAsyncClient()
video_payload: dict[str, Any] = {
"url": video_url,
"fps": fps,
}
if max_frames is not None:
video_payload["max_frames"] = max_frames
request_payload = {
"model": model_id,
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "video_url",
"video_url": video_payload,
},
],
}
],
"stream": False,
}
resp = await AsyncApiRequest("understand_video_content", client=use_client).request(
json=request_payload
)
resp.raise_for_status()
body = resp.json()
data = body.get("data") if isinstance(body, Mapping) else None
container = data if isinstance(data, Mapping) else body
choices = container.get("choices") if isinstance(container, Mapping) else None
if not isinstance(choices, list):
raise ValueError("understand_video_content 响应缺少 choices")
messages: list[dict[str, Any]] = []
for choice in choices:
if isinstance(choice, Mapping) and isinstance(choice.get("message"), Mapping):
messages.append(dict(choice["message"]))
return messages
async def mux_audio_video(
payload: MuxAudioVideoRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = (
payload.model_dump(mode="json", exclude_none=True)
if isinstance(payload, MuxAudioVideoRequest)
else MuxAudioVideoRequest.model_validate(payload).model_dump(
mode="json", exclude_none=True
)
)
return await AsyncApiRequest("mux_audio_video", client=use_client).request(
json=body
)
async def image_to_video(
payload: ImageToVideoRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = (
payload.model_dump(mode="json", exclude_none=True)
if isinstance(payload, ImageToVideoRequest)
else ImageToVideoRequest.model_validate(payload).model_dump(
mode="json", exclude_none=True
)
)
return await AsyncApiRequest("image_to_video", client=use_client).request(json=body)
async def extract_audio(
payload: ExtractAudioRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = (
payload.model_dump(mode="json", exclude_none=True)
if isinstance(payload, ExtractAudioRequest)
else ExtractAudioRequest.model_validate(payload).model_dump(
mode="json", exclude_none=True
)
)
return await AsyncApiRequest("extract_audio", client=use_client).request(json=body)
async def query_task(
task_id: str,
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
return await AsyncApiRequest("query_task", client=use_client).request(
path_params={"task_id": task_id}
)
def _extract_play_url(
task_type: str | None, parsed_result: Mapping[str, Any]
) -> str | None:
# 优先查找 video_url 和 audio_url,不管任务类型
video_url = parsed_result.get("video_url") or parsed_result.get("videoa_url")
audio_url = parsed_result.get("audio_url")
if video_url:
return video_url
if audio_url:
return audio_url
return parsed_result.get("play_url")
def normalize_query_task_response(
payload: Mapping[str, Any],
) -> QueryTaskNormalizedResult:
# 新接口:status / task_id / result 等在根上;旧接口:包在 Result 里
if isinstance(payload.get("status"), str):
task_block: Mapping[str, Any] = payload
else:
wrapped = payload.get("Result")
if not isinstance(wrapped, Mapping):
raise ValueError("query_task 响应缺少 status 或 Result 对象")
task_block = wrapped
status = task_block.get("status")
if not isinstance(status, str):
raise ValueError("query_task 响应缺少 status 字段")
# 新接口通常是根字段;旧接口可能在 Result 内
task_id = None
if isinstance(task_block.get("task_id"), str):
task_id = task_block["task_id"]
elif isinstance(task_block.get("TaskId"), str):
task_id = task_block["TaskId"]
elif isinstance(payload.get("task_id"), str):
task_id = payload["task_id"]
elif isinstance(payload.get("TaskId"), str):
task_id = payload["TaskId"]
task_type = (
task_block.get("task_type")
if isinstance(task_block.get("task_type"), str)
else None
)
request_id = None
if isinstance(task_block.get("request_id"), str):
request_id = task_block["request_id"]
else:
meta = payload.get("ResponseMetadata")
if isinstance(meta, Mapping) and isinstance(meta.get("RequestId"), str):
request_id = meta["RequestId"]
error = task_block.get("error")
if status in ("failed", "canceled"):
raise RuntimeError(str(error or f"task {status}"))
result_raw = task_block.get("result")
parsed_result: dict[str, Any] = {}
if isinstance(result_raw, str) and result_raw.strip():
parsed = json.loads(result_raw)
if isinstance(parsed, Mapping):
parsed_result = dict(parsed)
elif isinstance(result_raw, Mapping):
parsed_result = dict(result_raw)
return QueryTaskNormalizedResult(
task_id=task_id,
duration=parsed_result.get("duration"),
play_url=_extract_play_url(task_type, parsed_result),
request_id=request_id,
status=status, # type: ignore[arg-type]
task_type=task_type,
)
async def query_task_with_polling(
task_id: str,
interval: int = 5,
max_retries: int = 6,
*,
client: AmkAsyncClient | None = None,
) -> QueryTaskNormalizedResult:
"""
轮询任务直到完成或失败。
- status=running/queued: 继续轮询
- status=completed: 返回标准化结果
- status=failed/canceled: 抛异常
"""
if interval <= 0:
raise ValueError("interval 必须大于 0")
if max_retries <= 0:
raise ValueError("max_retries 必须大于 0")
for attempt in range(max_retries):
resp = await query_task(task_id, client=client)
resp.raise_for_status()
normalized = normalize_query_task_response(resp.json())
status: TaskStatusStr = normalized.status
if status == "completed":
return normalized
if status in ("running", "queued"):
if attempt < max_retries - 1:
await asyncio.sleep(interval)
continue
raise TimeoutError("task polling timeout")
raise RuntimeError(f"unsupported task status: {status}")
raise TimeoutError("task polling timeout")
async def enhance_video(
payload: EnhanceVideoRequest | Mapping[str, Any],
*,
client: AmkAsyncClient | None = None,
) -> httpx.Response:
use_client = client if client is not None else AmkAsyncClient()
body = (
payload.model_dump(mode="json", exclude_none=True)
if isinstance(payload, EnhanceVideoRequest)
else EnhanceVideoRequest.model_validate(payload).model_dump(
mode="json", exclude_none=True
)
)
return await AsyncApiRequest("enhance_video", client=use_client).request(json=body)
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from enum import Enum
class AmkAuthMode(str, Enum):
"""AMK HTTP API 鉴权注册方式(与网关约定一致)。"""
#: ``Authorization: Bearer <MediaKit API Key>``
MEDIAKIT_ONLY = "mediakit_only"
#: ``Authorization: Bearer <火山方舟 API Key>/<MediaKit API Key>``
ARK_AND_MEDIAKIT = "ark_and_mediakit"
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from typing import Any
import httpx
from amk_client.auth_mode import AmkAuthMode
from amk_client.config import (
build_authorization_header,
default_ark_api_key,
default_bearer_token,
normalize_secret,
require_credentials_for_mode,
resolve_base_url,
resolve_default_auth_mode,
)
def _load_dotenv() -> None:
try:
from dotenv import find_dotenv, load_dotenv
path = find_dotenv(usecwd=True)
if path:
load_dotenv(path)
except ImportError:
pass
class AmkClient:
"""
Sync HTTP client for AMK APIs.
**鉴权模式按「单次请求」指定**(与网关上各 API 注册方式一致),在
``request`` / ``get`` / ``post`` / ``put`` / ``delete`` / ``stream`` /
``iter_*`` 上传入 ``auth_mode=...``。
- ``AmkAuthMode.MEDIAKIT_ONLY``:``Authorization: Bearer <MediaKit API Key>``
- ``AmkAuthMode.ARK_AND_MEDIAKIT``:
``Authorization: Bearer <火山方舟 API Key>/<MediaKit API Key>``
若某次调用**未传** ``auth_mode``,则使用构造参数 ``auth_mode``(默认模式);
构造时也未指定时,再读环境变量 ``AMK_AUTH_MODE``。
密钥来自 ``AMK_API_KEY`` / ``ARK_API_KEY`` 或构造参数 ``api_key`` / ``ark_api_key``。
- Base URL 默认由 ``AMK_ENV`` 与当次 ``auth_mode`` 共同决定(同一环境下不同鉴权模式可命中不同 host 组);也可用构造参数 ``base_url`` 覆盖。
- ``require_api_key=True``(默认)时,**每次请求**按当次 ``auth_mode`` 校验所需密钥。
- ``require_api_key=False`` 时不校验;有密钥则仍按当次模式组 ``Authorization``。
- 单次请求可传 ``headers`` 覆盖 ``Authorization``。
"""
def __init__(
self,
*,
auth_mode: AmkAuthMode | None = None,
base_url: str | None = None,
api_key: str | None = None,
ark_api_key: str | None = None,
require_api_key: bool = True,
timeout: float | httpx.Timeout | None = 30.0,
extra_headers: Mapping[str, str] | None = None,
) -> None:
_load_dotenv()
# 仅当具体请求未传 auth_mode 时使用(非「全局业务配置」,而是默认值)
self._default_auth_mode = (
auth_mode if auth_mode is not None else resolve_default_auth_mode()
)
self._base_override = base_url.rstrip("/") if base_url else None
self._mediakit_explicit = api_key
self._ark_explicit = ark_api_key
self._require_api_key = require_api_key
self._timeout = timeout
self._extra_headers = dict(extra_headers) if extra_headers else {}
def _mediakit_resolved(self) -> str | None:
if self._mediakit_explicit is not None:
return normalize_secret(self._mediakit_explicit)
return default_bearer_token()
def _ark_resolved(self) -> str | None:
if self._ark_explicit is not None:
return normalize_secret(self._ark_explicit)
return default_ark_api_key()
def _authorization_for_mode(self, mode: AmkAuthMode) -> str | None:
mk = self._mediakit_resolved()
if mode == AmkAuthMode.MEDIAKIT_ONLY:
if not mk:
return None
return build_authorization_header(
mediakit_api_key=mk, mode=AmkAuthMode.MEDIAKIT_ONLY
)
ark = self._ark_resolved()
if not mk or not ark:
return None
return build_authorization_header(
mediakit_api_key=mk,
ark_api_key=ark,
mode=AmkAuthMode.ARK_AND_MEDIAKIT,
)
def _effective_auth_mode(self, auth_mode: AmkAuthMode | None) -> AmkAuthMode:
return auth_mode if auth_mode is not None else self._default_auth_mode
def _merge_headers(
self,
call_headers: Mapping[str, str] | None,
*,
auth_mode: AmkAuthMode | None = None,
) -> dict[str, str]:
mode = self._effective_auth_mode(auth_mode)
if self._require_api_key:
require_credentials_for_mode(
mode,
mediakit_from_constructor=self._mediakit_explicit,
ark_from_constructor=self._ark_explicit,
)
merged: dict[str, str] = {**self._extra_headers}
auth = self._authorization_for_mode(mode)
if auth:
merged.setdefault("Authorization", auth)
# 默认使用 JSON 请求体;调用方可在单次请求 headers 中覆盖
merged.setdefault("Content-Type", "application/json")
if call_headers:
merged.update(call_headers)
return merged
def _url(self, path: str, *, auth_mode: AmkAuthMode | None = None) -> str:
p = path if path.startswith("/") else f"/{path}"
mode = self._effective_auth_mode(auth_mode)
base = self._base_override or resolve_base_url(
auth_mode=mode, fallback_env="prod"
)
return f"{base}{p}"
def _client(self) -> httpx.Client:
return httpx.Client(timeout=self._timeout)
def request(
self,
method: str,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
with self._client() as client:
return client.request(
method.upper(),
self._url(path, auth_mode=auth_mode),
headers=self._merge_headers(headers, auth_mode=auth_mode),
**kwargs,
)
def get(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return self.request("GET", path, auth_mode=auth_mode, headers=headers, **kwargs)
def post(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return self.request(
"POST", path, auth_mode=auth_mode, headers=headers, **kwargs
)
def put(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return self.request("PUT", path, auth_mode=auth_mode, headers=headers, **kwargs)
def delete(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return self.request(
"DELETE", path, auth_mode=auth_mode, headers=headers, **kwargs
)
@contextmanager
def stream(
self,
method: str,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> Iterator[httpx.Response]:
"""Stream response body (SSE/chunked). Caller should read ``iter_bytes`` / ``iter_lines``."""
with self._client() as client:
with client.stream(
method.upper(),
self._url(path, auth_mode=auth_mode),
headers=self._merge_headers(headers, auth_mode=auth_mode),
**kwargs,
) as response:
yield response
def iter_bytes(
self,
method: str,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
chunk_size: int | None = None,
**kwargs: Any,
) -> Iterator[bytes]:
"""Convenience: yield byte chunks from a streaming request."""
with self.stream(
method, path, auth_mode=auth_mode, headers=headers, **kwargs
) as resp:
yield from resp.iter_bytes(chunk_size=chunk_size)
def iter_lines(
self,
method: str,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> Iterator[str]:
"""Convenience: yield text lines (e.g. SSE) from a streaming request."""
with self.stream(
method, path, auth_mode=auth_mode, headers=headers, **kwargs
) as resp:
yield from resp.iter_lines()
class AmkAsyncClient(AmkClient):
"""Async HTTP client for AMK APIs."""
def _async_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(timeout=self._timeout)
async def request(
self,
method: str,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
async with self._async_client() as client:
return await client.request(
method.upper(),
self._url(path, auth_mode=auth_mode),
headers=self._merge_headers(headers, auth_mode=auth_mode),
**kwargs,
)
async def get(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return await self.request(
"GET", path, auth_mode=auth_mode, headers=headers, **kwargs
)
async def post(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return await self.request(
"POST", path, auth_mode=auth_mode, headers=headers, **kwargs
)
async def put(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return await self.request(
"PUT", path, auth_mode=auth_mode, headers=headers, **kwargs
)
async def delete(
self,
path: str,
*,
auth_mode: AmkAuthMode | None = None,
headers: Mapping[str, str] | None = None,
**kwargs: Any,
) -> httpx.Response:
return await self.request(
"DELETE", path, auth_mode=auth_mode, headers=headers, **kwargs
)
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import os
from amk_client.auth_mode import AmkAuthMode
HOSTS_BY_AUTH_MODE: dict[AmkAuthMode, dict[str, str]] = {
AmkAuthMode.MEDIAKIT_ONLY: {
"prod": "https://amk.cn-beijing.volces.com",
},
AmkAuthMode.ARK_AND_MEDIAKIT: {
"prod": "https://amk-ark.cn-beijing.volces.com",
},
}
DEFAULT_AMK_ENV = "prod"
DEFAULT_AMK_AUTH_MODE = AmkAuthMode.MEDIAKIT_ONLY
# 控制台入口,供缺失/错误配置时的 reference_url
AMK_CONSOLE_REFERENCE = "https://console.volcengine.com/imp/ai-mediakit/"
ARK_CONSOLE_REFERENCE = "https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seed-1-8"
class MissingConfigError(ValueError):
"""缺少或非法配置。``var`` 供程序判断;``args[0]`` 为结构化说明,便于人与 Agent 阅读。"""
var: str
@classmethod
def missing_env(
cls,
name: str,
*,
purpose: str,
reference_url: str | None = None,
value_placeholder: str = "<值>",
) -> MissingConfigError:
steps = (
f"在仓库根目录的 `.env` 增加一行: {name}={value_placeholder}",
f"或在本终端执行: export {name}={value_placeholder}(再重新运行命令)",
"若由 Agent 处理:向用户索取真实密钥后,直接编辑仓库根目录 `.env` 写入该键值,"
f"或在执行命令的 shell 中 `export {name}=<密钥>`,然后重试;不要仅复述本错误而不改配置。",
)
lines = [
f"missing_env_var: {name}",
f"purpose: {purpose}",
"fix:",
*[f" - {s}" for s in steps],
"agent_instruction: Obtain the secret from the user, then add `"
f"{name}=<actual_value>` to repo-root `.env` or export it in the shell, and retry; do not stop after parroting this error.",
]
if reference_url:
lines.append(f"reference_url: {reference_url}")
msg = "\n".join(lines)
err = cls(msg)
err.var = name
return err
@classmethod
def invalid_env_value(
cls,
name: str,
value: str,
*,
allowed: tuple[str, ...],
purpose: str,
) -> MissingConfigError:
lines = [
f"invalid_env_var: {name}",
f"given_value: {value!r}",
f"allowed: {', '.join(allowed)}",
f"purpose: {purpose}",
"fix:",
" - 将环境变量设为 allowed 中之一;若未设置该变量,将使用默认 `prod`。",
f"agent_instruction: Edit repo-root `.env` (or export) so `{name}` is one of: "
f"{', '.join(allowed)}; or remove `{name}` to use default `prod`, then retry.",
]
msg = "\n".join(lines)
err = cls(msg)
err.var = name
return err
def _read_amk_env(*, auth_mode: AmkAuthMode) -> str:
raw = os.environ.get("AMK_ENV")
if raw is None or not raw.strip():
return DEFAULT_AMK_ENV
v = raw.strip().lower()
allowed_envs = HOSTS_BY_AUTH_MODE[auth_mode]
if v not in allowed_envs:
raise MissingConfigError.invalid_env_value(
"AMK_ENV",
v,
allowed=tuple(sorted(allowed_envs)),
purpose="选择 AMK 网关环境:当前仅支持 `prod`(线上环境)。",
)
return v
def resolve_default_auth_mode() -> AmkAuthMode:
"""从 ``AMK_AUTH_MODE`` 解析 **AmkClient 的默认** 鉴权模式(仅当单次请求未传 ``auth_mode``、且构造时也未指定 ``auth_mode`` 时生效);未设置环境变量时为 ``mediakit_only``。
合法取值:``mediakit_only``(或 ``mediakit`` / ``single``)、
``ark_and_mediakit``(或 ``ark_mediakit`` / ``dual`` / ``combined``)。
"""
raw = (os.environ.get("AMK_AUTH_MODE") or "").strip().lower()
if raw in ("", "mediakit_only", "mediakit", "single"):
return AmkAuthMode.MEDIAKIT_ONLY
if raw in ("ark_and_mediakit", "ark_mediakit", "dual", "combined", "ark+mediakit"):
return AmkAuthMode.ARK_AND_MEDIAKIT
raise MissingConfigError.invalid_env_value(
"AMK_AUTH_MODE",
raw or "(empty)",
allowed=("mediakit_only", "ark_and_mediakit"),
purpose="注册 API 时选择的鉴权方式:仅 MediaKit Key,或 方舟 Key + MediaKit Key。",
)
def build_authorization_header(
*,
mediakit_api_key: str,
ark_api_key: str | None = None,
mode: AmkAuthMode = DEFAULT_AMK_AUTH_MODE,
) -> str:
"""组装 ``Authorization`` 请求头的完整值(含 ``Bearer `` 前缀)。"""
if mode == AmkAuthMode.MEDIAKIT_ONLY:
return f"Bearer {mediakit_api_key}"
if ark_api_key is None:
raise ValueError("ark_and_mediakit 模式下必须提供 ark_api_key")
return f"Bearer {ark_api_key}/{mediakit_api_key}"
def resolve_base_url(
*,
auth_mode: AmkAuthMode = DEFAULT_AMK_AUTH_MODE,
fallback_env: str = DEFAULT_AMK_ENV,
) -> str:
"""Return API base URL for current ``AMK_ENV`` and auth mode.
若显式设置了非法的 ``AMK_ENV``,抛出 ``MissingConfigError``。
"""
env = _read_amk_env(auth_mode=auth_mode)
hosts = HOSTS_BY_AUTH_MODE[auth_mode]
return hosts.get(env, hosts[fallback_env])
def normalize_secret(value: str | None) -> str | None:
"""非空、非 ``null`` 的字符串视为有效密钥;否则 ``None``。"""
if value is None:
return None
stripped = value.strip()
if not stripped or stripped.lower() == "null":
return None
return stripped
def default_bearer_token() -> str | None:
"""Token from ``AMK_API_KEY`` (empty or ``null`` ignored)."""
return normalize_secret(os.environ.get("AMK_API_KEY"))
def require_amk_api_key() -> str:
"""返回可用的 MediaKit / AMK ``AMK_API_KEY``;缺失时抛出 ``MissingConfigError``。"""
key = default_bearer_token()
if key is None:
raise MissingConfigError.missing_env(
"AMK_API_KEY",
purpose="调用 AMK(MediaKit)HTTP API 时的 Bearer 鉴权。",
reference_url=AMK_CONSOLE_REFERENCE,
value_placeholder="<MediaKit API Key>",
)
return key
def default_ark_api_key() -> str | None:
"""方舟 ``ARK_API_KEY``;未设置、空或 ``null`` 时返回 ``None``。"""
return normalize_secret(os.environ.get("ARK_API_KEY"))
def require_ark_api_key() -> str:
"""返回可用的方舟密钥;缺失时抛出 ``MissingConfigError``。"""
key = default_ark_api_key()
if key is None:
raise MissingConfigError.missing_env(
"ARK_API_KEY",
purpose="方舟 + MediaKit 双 Key 鉴权时,作为 ``Authorization: Bearer <方舟>/<MediaKit>`` 的前段。",
reference_url=ARK_CONSOLE_REFERENCE,
value_placeholder="<Ark API Key>",
)
return key
def require_credentials_for_mode(
mode: AmkAuthMode,
*,
mediakit_from_constructor: str | None,
ark_from_constructor: str | None,
) -> None:
"""按鉴权模式校验必填密钥(构造参数或环境变量);缺一则抛 ``MissingConfigError``。"""
mk = (
normalize_secret(mediakit_from_constructor)
if mediakit_from_constructor is not None
else default_bearer_token()
)
if mode == AmkAuthMode.MEDIAKIT_ONLY:
if (
mediakit_from_constructor is not None
and normalize_secret(mediakit_from_constructor) is None
):
raise MissingConfigError.missing_env(
"AMK_API_KEY",
purpose="单 MediaKit Key 鉴权,或双 Key 模式下的 MediaKit 段。",
reference_url=AMK_CONSOLE_REFERENCE,
value_placeholder="<MediaKit API Key>",
)
if mk is None:
require_amk_api_key()
return
# ARK_AND_MEDIAKIT
if (
mediakit_from_constructor is not None
and normalize_secret(mediakit_from_constructor) is None
):
raise MissingConfigError.missing_env(
"AMK_API_KEY",
purpose="双 Key 鉴权:``Authorization: Bearer <方舟 API Key>/<MediaKit API Key>`` 的后段。",
reference_url=AMK_CONSOLE_REFERENCE,
value_placeholder="<MediaKit API Key>",
)
if mk is None:
require_amk_api_key()
ark = (
normalize_secret(ark_from_constructor)
if ark_from_constructor is not None
else default_ark_api_key()
)
if (
ark_from_constructor is not None
and normalize_secret(ark_from_constructor) is None
):
raise MissingConfigError.missing_env(
"ARK_API_KEY",
purpose="双 Key 鉴权:``Authorization: Bearer <方舟 API Key>/<MediaKit API Key>`` 的前段。",
reference_url=ARK_CONSOLE_REFERENCE,
value_placeholder="<Ark API Key>",
)
if ark is None:
require_ark_api_key()
"""AMK 相关 Pydantic 模型(请求体、响应体、路径参数等,随 API 扩展)。"""
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from typing import Any, Literal
from pydantic import (
AliasChoices,
BaseModel,
ConfigDict,
Field,
ValidationInfo,
field_validator,
)
TrimMediaKind = Literal["audio", "video"]
class TrimMediaDurationRequest(BaseModel):
"""``POST /api/v1/tools/trim_media_duration`` 请求体。
Args:
type (str): **必选字段**,裁剪类型。``audio`` | ``video``。
source (str): **必选字段**,待剪切的资源 URL,支持 ``http://`` 或 ``https://`` 格式。
start_time (int | float): **非必选字段**,裁剪开始时间,默认为 ``0``,表示从头开始裁剪;
最多保留 2 位小数,单位:秒。
end_time (int | float | None): **非必选字段**,裁剪结束时间,默认为片源结尾;
最多保留 2 位小数,单位:秒;省略或 ``null`` 表示至片尾。
"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
type: TrimMediaKind = Field(
...,
description="裁剪类型(与调用侧聚合节点一致):audio | video",
)
source: str = Field(..., min_length=1, description="待裁剪资源 URL")
start_time: float = Field(
default=0.0, ge=0, description="裁剪开始时间(秒),默认从头"
)
end_time: float | None = Field(
default=None,
ge=0,
description="裁剪结束时间(秒),省略表示至片源结尾",
)
@field_validator("type", mode="before")
@classmethod
def type_must_be_audio_or_video_str(cls, v: Any) -> Any:
if v is None:
raise ValueError('type 不能为空;须为字符串 "audio" 或 "video"')
if not isinstance(v, str):
raise ValueError(
f'type 须为字符串类型(取值 "audio" | "video"),当前为 {type(v).__name__}'
)
return v
@field_validator("source", mode="before")
@classmethod
def source_must_be_str(cls, v: Any) -> Any:
if v is None:
raise ValueError(
"source 不能为空;须为字符串类型,且为 http:// 或 https:// 开头的资源 URL"
)
if not isinstance(v, str):
raise ValueError(
f"source 须为字符串类型(资源 URL),当前为 {type(v).__name__};"
"若为多段资源请使用对应接口的数组字段,勿把列表误传到 source"
)
return v
@field_validator("source")
@classmethod
def source_must_be_http(cls, v: str) -> str:
s = v.strip()
if not (s.startswith("http://") or s.startswith("https://")):
raise ValueError("source 须为 http:// 或 https:// 开头的 URL 字符串")
return s
@field_validator("start_time", "end_time", mode="before")
@classmethod
def time_fields_must_be_number_or_omit(cls, v: Any, info: ValidationInfo) -> Any:
field = info.field_name
if v is None:
if field == "end_time":
return None
raise ValueError(
"start_time 不能为空(勿传 null);须为数字类型(秒,≥0),"
"若从片头开始请省略该字段或传 0"
)
if not isinstance(v, (int, float)):
raise ValueError(f"{field} 须为数字类型(秒),当前为 {type(v).__name__}")
return v
@field_validator("start_time", "end_time")
@classmethod
def at_most_two_decimal_places(cls, v: float | None) -> float | None:
if v is None:
return v
text = f"{v:.4f}".rstrip("0").rstrip(".")
if "." in text:
_, frac = text.split(".", 1)
if len(frac) > 2:
raise ValueError("时间最多保留 2 位小数(单位:秒)")
return v
def to_api_json(self) -> dict[str, Any]:
"""发给网关的 JSON:省略 ``end_time`` 表示默认到片尾。"""
return self.model_dump(mode="json", by_alias=True, exclude_none=True)
class TrimMediaDurationResponse(BaseModel):
"""``trim_media_duration`` 返回中的任务信息。
Returns:
task_id: 任务查询 id。
request_id: 日志 id(兼容历史网关字段名 ``requst_id``)。
"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
task_id: str = Field(..., description="任务查询 id")
request_id: str = Field(
...,
validation_alias=AliasChoices("request_id", "requst_id"),
description="日志 id",
)
@field_validator("task_id", "request_id", mode="before")
@classmethod
def response_string_ids(cls, v: Any, info: ValidationInfo) -> Any:
name = info.field_name
if v is None:
raise ValueError(f"{name} 不能为空;解析响应时期望非空字符串")
if not isinstance(v, str):
raise ValueError(f"{name} 须为字符串类型,当前为 {type(v).__name__}")
return v
class TrimMediaDurationAsyncResult(BaseModel):
"""调用节点聚合接口的异步返回结构。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
task_id: str | None = Field(default=None, description="任务查询 id")
request_id: str | None = Field(default=None, description="日志 id")
error: str | None = Field(default=None, description="报错信息")
ConcatMediaKind = Literal["audio", "video"]
TransitionId = Literal[
"1182359",
"1182360",
"1182358",
"1182365",
"1182367",
"1182368",
"1182369",
"1182370",
"1182373",
"1182374",
"1182375",
"1182378",
]
class ConcatMediaSegmentsRequest(BaseModel):
"""调用层聚合接口 ``concat_media_segments`` 请求体。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
type: ConcatMediaKind = Field(
...,
description="必选,拼接类型:audio | video",
)
sources: list[str] = Field(
...,
min_length=1,
max_length=100,
description="必选,待拼接资源 URL 列表(http:// 或 https://)",
)
transitions: list[TransitionId] | None = Field(
default=None,
description="可选,转场效果 ID 列表;音频拼接不支持",
)
@field_validator("sources", mode="before")
@classmethod
def sources_must_be_string_list(cls, v: Any) -> Any:
if not isinstance(v, list):
raise ValueError("sources 须为字符串 URL 列表")
if not v:
raise ValueError("sources 不能为空;至少提供 1 个资源 URL")
return v
@field_validator("sources")
@classmethod
def sources_each_must_be_http_url(cls, values: list[str]) -> list[str]:
for idx, item in enumerate(values):
if not isinstance(item, str):
raise ValueError(f"sources[{idx}] 须为字符串 URL")
s = item.strip()
if not (s.startswith("http://") or s.startswith("https://")):
raise ValueError(f"sources[{idx}] 须为 http:// 或 https:// 开头的 URL")
return values
@field_validator("transitions", mode="before")
@classmethod
def transitions_must_be_string_list_or_none(cls, v: Any) -> Any:
if v is None:
return None
if not isinstance(v, list):
raise ValueError("transitions 须为字符串列表或省略")
for idx, item in enumerate(v):
if not isinstance(item, str):
raise ValueError(f"transitions[{idx}] 须为字符串转场 ID")
return v
@field_validator("transitions")
@classmethod
def audio_should_not_pass_transitions(
cls, v: list[str] | None, info: ValidationInfo
) -> list[str] | None:
media_type = info.data.get("type")
if media_type == "audio" and v:
raise ValueError("音频拼接不支持 transitions,请不要传该字段")
return v
def to_api_json(self) -> dict[str, Any]:
"""发给底层 API 的 JSON;不必要字段自动省略。"""
return self.model_dump(mode="json", by_alias=True, exclude_none=True)
class ConcatMediaSegmentsResponse(BaseModel):
"""``concat_media_segments`` 聚合后的任务返回。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
task_id: str = Field(..., description="任务查询 id")
request_id: str = Field(
...,
validation_alias=AliasChoices("request_id", "requst_id"),
description="日志 id",
)
class ConcatMediaSegmentsAsyncResult(BaseModel):
"""调用层聚合方法的异步返回结构。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
task_id: str | None = Field(default=None, description="任务查询 id")
request_id: str | None = Field(default=None, description="日志 id")
error: str | None = Field(default=None, description="报错信息")
class MuxAudioVideoRequest(BaseModel):
"""``mux_audio_video`` 请求体。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
video_url: str = Field(..., description="输入视频 URL")
audio_url: str = Field(..., description="输入音频 URL")
is_audio_reserve: bool = Field(default=True, description="是否保留原视频音频")
is_video_audio_sync: bool = Field(default=False, description="是否对齐音视频时长")
sync_mode: Literal["video", "audio"] = Field(
default="video", description="对齐基准"
)
sync_method: Literal["speed", "trim"] = Field(
default="trim", description="对齐方式"
)
class ImageToVideoImageItem(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
image_url: str = Field(..., description="图片 URL")
duration: float | None = Field(default=3.0, ge=0, description="播放时长(秒)")
animation_type: (
Literal[
"move_up", "move_down", "move_left", "move_right", "zoom_in", "zoom_out"
]
| None
) = Field(default=None, description="动画类型")
animation_in: float | None = Field(
default=None, ge=0, description="动画开始时间(秒)"
)
animation_out: float | None = Field(
default=None, ge=0, description="动画结束时间(秒)"
)
class ImageToVideoRequest(BaseModel):
"""``image_to_video`` 请求体。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
images: list[ImageToVideoImageItem] = Field(..., min_length=1, max_length=100)
transitions: list[TransitionId] | None = Field(
default=None, description="可选转场效果 ID"
)
class ExtractAudioRequest(BaseModel):
"""``extract_audio`` 请求体。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
video_url: str = Field(..., description="输入视频 URL")
format: Literal["mp3", "m4a"] = Field(default="m4a", description="输出音频格式")
class EnhanceVideoRequest(BaseModel):
"""``enhance_video`` 请求体。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
video_url: str = Field(..., description="输入视频 URL")
tool_version: Literal["standard", "professional"] = Field(
default="standard",
description="工具版本:standard 标准版,professional 专业版",
)
resolution: (
Literal["240p", "360p", "480p", "540p", "720p", "1080p", "2k", "4k"] | None
) = Field(
default=None,
description="目标分辨率;不传/不填写则使用原始分辨率(后端按原分辨率处理)",
)
resolution_limit: int | None = Field(
default=None, ge=64, le=2160, description="目标长宽限制"
)
fps: float | None = Field(default=None, gt=0, le=120, description="目标帧率")
TaskStatusStr = Literal["running", "completed", "queued", "failed", "canceled"]
class QueryTaskNormalizedResult(BaseModel):
"""任务查询的标准化返回结构。"""
model_config = ConfigDict(str_strip_whitespace=True, populate_by_name=True)
task_id: str | None = Field(default=None, description="任务查询 ID")
duration: float | None = Field(default=None, description="时长(秒)")
play_url: str | None = Field(default=None, description="播放地址")
request_id: str | None = Field(default=None, description="日志 id")
status: TaskStatusStr = Field(..., description="任务状态")
task_type: str | None = Field(default=None, description="任务类型")
annotated-types==0.7.0
anyio==4.13.0
certifi==2026.2.25
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.11
pydantic==2.12.5
pydantic-core==2.41.5
python-dotenv==1.2.2
typing-extensions==4.15.0
typing-inspection==0.4.2
火山引擎 AI MediaKit(AMK)与方舟 ARK
AMK 访问密钥(云端能力需要;裁剪/拼接/提取/合成可在缺失时自动走本地 FFmpeg)
AMK_API_KEY=
AMK 服务端环境:仅支持 prod(生产)
AMK_ENV=prod
是否为除「视频理解」外的异步请求自动注入 8 位 client_token(幂等);仅支持 true 或 false
AMK_ENABLE_CLIENT_TOKEN=false
方舟 OpenAPI 密钥(可选;使用 understand_video_content 视频理解时必填)
ARK_API_KEY=
方舟模型 ID(可选;使用 understand_video_content 视频理解时必填,对应请求体 model 字段)
ARK_MODEL_ID=
控制台参考(勿将真实密钥提交到仓库)
AI MediaKit:https://console.volcengine.com/imp/ai-mediakit/
方舟模型与密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seed-1-8
Related skills
FAQ
How does it choose cloud vs local?
Trim, concat, audio extract and mux run on AMK cloud when configured with URL inputs, else local FFmpeg; flip, speed, subtitle, watermark and transcode are always local FFmpeg.
What is needed for video understanding?
Video understanding requires an ARK_API_KEY and an ARK_MODEL_ID (for example doubao-seed-1-8).