feat: 真人人像素材管理模块初始版本
- 支持火山方舟(Volcengine Ark)真人人像素材API - AK/SK HMAC-SHA256签名(纯stdlib实现) - 素材组合(Asset Group)管理: 创建认证、查询、删除 - 素材资产(Asset)管理: 上传、状态同步、删除 - 多供应商可扩展架构 - 完整CRUD + 前端UI + uapi SQL配置 - 12个API端点 + 6个前端页面 - 数据库表: rl_asset_group, rl_asset
This commit is contained in:
commit
0e5696f5da
141
README.md
Normal file
141
README.md
Normal file
@ -0,0 +1,141 @@
|
||||
# reallife_asset - 真人人像素材管理模块
|
||||
|
||||
Sage平台的真人人像素材管理模块,支持多供应商(火山方舟等)的真人人像素材资产(Asset Group / Asset)管理。
|
||||
|
||||
## 功能概述
|
||||
|
||||
- **真人认证**: 创建H5认证链接,终端用户完成真人认证后获取素材组合ID
|
||||
- **素材上传**: 上传真人人像素材(图片/视频/音频),系统自动进行面部一致性比对
|
||||
- **状态同步**: 从供应商同步素材组合和素材资产状态
|
||||
- **多供应商**: 可扩展架构,当前支持火山方舟,预留可灵等供应商接口
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
reallife_asset/
|
||||
├── reallife_asset/ # Python包
|
||||
│ ├── __init__.py
|
||||
│ ├── init.py # 模块初始化 + 业务逻辑
|
||||
│ └── volcengine_client.py # 火山方舟API客户端(AK/SK签名)
|
||||
├── models/ # 数据库表定义
|
||||
│ ├── rl_asset_group.json # 素材组合表
|
||||
│ └── rl_asset.json # 素材资产表
|
||||
├── json/ # CRUD定义
|
||||
│ ├── rl_asset_group_list.json
|
||||
│ └── rl_asset_list.json
|
||||
├── wwwroot/ # 前端
|
||||
│ ├── index.ui # 模块入口
|
||||
│ ├── group_manage.ui # 组合管理页
|
||||
│ ├── asset_manage.ui # 资产管理页
|
||||
│ ├── create_validate.ui # 创建认证页
|
||||
│ ├── upload_asset.ui # 上传素材页
|
||||
│ ├── sync_groups.ui # 同步页
|
||||
│ └── api/ # API端点
|
||||
│ ├── rl_asset_group_create.dspy
|
||||
│ ├── rl_asset_group_update.dspy
|
||||
│ ├── rl_asset_group_delete.dspy
|
||||
│ ├── rl_asset_create.dspy
|
||||
│ ├── rl_asset_update.dspy
|
||||
│ ├── rl_asset_delete.dspy
|
||||
│ ├── sync_asset_status.dspy
|
||||
│ ├── check_validate.dspy
|
||||
│ ├── sync_from_vendor.dspy
|
||||
│ ├── sync_assets.dspy
|
||||
│ ├── get_rl_asset_group_list.dspy
|
||||
│ └── get_rl_asset_list.dspy
|
||||
├── init/
|
||||
│ └── data.json # 初始化编码数据
|
||||
├── scripts/
|
||||
│ ├── load_path.py # RBAC权限注册
|
||||
│ └── uapi_volcengine_ark.sql # uapi接口SQL配置
|
||||
├── pyproject.toml
|
||||
├── build.sh
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 数据库表
|
||||
|
||||
### rl_asset_group (素材组合表)
|
||||
- 记录真人认证组合,每个组合对应一个真人
|
||||
- 字段: vendor, vendor_group_id, name, status, byted_token, h5_link等
|
||||
|
||||
### rl_asset (素材资产表)
|
||||
- 记录具体素材文件,关联到组合
|
||||
- 字段: vendor, vendor_asset_id, asset_type, status, url, asset_uri等
|
||||
|
||||
## API接口
|
||||
|
||||
### 火山方舟 API (通过uapi系统)
|
||||
|
||||
| API | 功能 | 限流 |
|
||||
|-----|------|------|
|
||||
| CreateVisualValidateSession | 创建H5认证页 | 3 QPS |
|
||||
| GetVisualValidateResult | 获取认证结果 | 3 QPS |
|
||||
| CreateAsset | 上传素材(异步) | 按权益 |
|
||||
| GetAsset | 查询素材 | 100 QPS |
|
||||
| ListAssets | 素材列表 | 10 QPS |
|
||||
| ListAssetGroups | 组合列表 | 10 QPS |
|
||||
| GetAssetGroup | 查询组合 | 10 QPS |
|
||||
| UpdateAsset | 更新素材 | 10 QPS |
|
||||
| UpdateAssetGroup | 更新组合 | 10 QPS |
|
||||
| DeleteAsset | 删除素材 | 10 QPS |
|
||||
| DeleteAssetGroup | 删除组合 | 5 QPS |
|
||||
|
||||
## 安装部署
|
||||
|
||||
### 1. 安装模块
|
||||
```bash
|
||||
cd ~/repos/reallife_asset
|
||||
bash build.sh
|
||||
```
|
||||
|
||||
### 2. 创建数据库表
|
||||
```bash
|
||||
# 执行DDL
|
||||
mysql -u root -p sage < mysql.ddl.sql
|
||||
```
|
||||
|
||||
### 3. 注册RBAC权限
|
||||
```bash
|
||||
cd ~/repos/sage
|
||||
./py3/bin/python ~/repos/reallife_asset/scripts/load_path.py
|
||||
```
|
||||
|
||||
### 4. 配置uapi接口(可选)
|
||||
```bash
|
||||
mysql -u root -p sage < ~/repos/reallife_asset/scripts/uapi_volcengine_ark.sql
|
||||
```
|
||||
|
||||
### 5. 集成到Sage
|
||||
在 `app/sage.py` 中添加:
|
||||
```python
|
||||
from reallife_asset.init import load_reallife_asset
|
||||
# 在init()中:
|
||||
load_reallife_asset()
|
||||
```
|
||||
|
||||
### 6. 重启Sage
|
||||
```bash
|
||||
cd ~/repos/sage && ./stop.sh && ./start.sh
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. **创建真人认证**: 提供回调URL和AK/SK → 获取H5认证链接
|
||||
2. **终端用户认证**: 用户通过H5链接完成真人认证
|
||||
3. **获取组合ID**: 认证成功后查询获取供应商端Group ID
|
||||
4. **上传素材**: 向已认证的组合上传图片/视频素材
|
||||
5. **同步状态**: 轮询素材处理状态直到Active
|
||||
6. **使用素材**: 使用 `asset://<asset_id>` URI 进行视频生成
|
||||
|
||||
## 扩展新供应商
|
||||
|
||||
1. 在 `volcengine_client.py` 中实现新的客户端类
|
||||
2. 在 `_PROVIDERS` 字典中注册
|
||||
3. 添加对应的uapi SQL配置
|
||||
|
||||
## 技术要点
|
||||
|
||||
- AK/SK签名: 使用HMAC-SHA256实现火山方舟V4签名(纯stdlib,无外部依赖)
|
||||
- 多租户: 所有数据按org_id隔离
|
||||
- 异步处理: CreateAsset为异步接口,需轮询状态
|
||||
63
build.sh
Executable file
63
build.sh
Executable file
@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Find Sage root
|
||||
SAGE_ROOT=""
|
||||
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
|
||||
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
|
||||
SAGE_ROOT="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$SAGE_ROOT" ]; then
|
||||
echo "ERROR: Cannot find Sage root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Sage root: $SAGE_ROOT"
|
||||
MODULE_NAME="reallife_asset"
|
||||
MODULE_WWWROOT="$SAGE_ROOT/wwwroot/$MODULE_NAME"
|
||||
|
||||
# Install module
|
||||
cd "$SCRIPT_DIR"
|
||||
"$SAGE_ROOT/py3/bin/pip" install .
|
||||
|
||||
# Create wwwroot directory if not exists
|
||||
mkdir -p "$MODULE_WWWROOT/api"
|
||||
|
||||
# Link UI files
|
||||
for f in "$SCRIPT_DIR/wwwroot"/*.ui; do
|
||||
[ -f "$f" ] || continue
|
||||
ln -sf "$f" "$MODULE_WWWROOT/"
|
||||
done
|
||||
|
||||
# Link API dspy files
|
||||
for f in "$SCRIPT_DIR/wwwroot/api"/*.dspy; do
|
||||
[ -f "$f" ] || continue
|
||||
ln -sf "$f" "$MODULE_WWWROOT/api/"
|
||||
done
|
||||
|
||||
# Generate DDL
|
||||
if [ -d "$SCRIPT_DIR/models" ]; then
|
||||
cd "$SCRIPT_DIR/models"
|
||||
"$SAGE_ROOT/py3/bin/json2ddl" mysql . > "$SCRIPT_DIR/mysql.ddl.sql" 2>/dev/null || true
|
||||
echo "DDL generated: $SCRIPT_DIR/mysql.ddl.sql"
|
||||
fi
|
||||
|
||||
# Generate CRUD UI (if json/ has files)
|
||||
if [ -d "$SCRIPT_DIR/json" ] && ls "$SCRIPT_DIR/json"/*.json >/dev/null 2>&1; then
|
||||
cd "$SCRIPT_DIR/json"
|
||||
"$SAGE_ROOT/py3/bin/xls2ui" -m "$SCRIPT_DIR/models" -o "$SCRIPT_DIR/wwwroot" "$MODULE_NAME" *.json 2>/dev/null || true
|
||||
# Link generated CRUD directories
|
||||
for d in "$SCRIPT_DIR/wwwroot"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
dname=$(basename "$d")
|
||||
case "$dname" in api|styles|scripts) continue ;; esac
|
||||
ln -sf "$d" "$MODULE_WWWROOT/$dname"
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Build complete: $MODULE_NAME"
|
||||
98
init/data.json
Normal file
98
init/data.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"id": "rl_vendor",
|
||||
"name": "真人素材供应商",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "rl_group_status",
|
||||
"name": "素材组合状态",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "rl_asset_type",
|
||||
"name": "素材类型",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "rl_asset_status",
|
||||
"name": "素材状态",
|
||||
"hierarchy_flg": "0"
|
||||
}
|
||||
],
|
||||
"appcodes_kv": [
|
||||
{
|
||||
"id": "rl_vendor_volcengine",
|
||||
"parentid": "rl_vendor",
|
||||
"k": "volcengine",
|
||||
"v": "火山方舟"
|
||||
},
|
||||
{
|
||||
"id": "rl_vendor_kling",
|
||||
"parentid": "rl_vendor",
|
||||
"k": "kling",
|
||||
"v": "可灵"
|
||||
},
|
||||
{
|
||||
"id": "rl_vendor_other",
|
||||
"parentid": "rl_vendor",
|
||||
"k": "other",
|
||||
"v": "其他"
|
||||
},
|
||||
{
|
||||
"id": "rl_gs_active",
|
||||
"parentid": "rl_group_status",
|
||||
"k": "active",
|
||||
"v": "正常"
|
||||
},
|
||||
{
|
||||
"id": "rl_gs_pending",
|
||||
"parentid": "rl_group_status",
|
||||
"k": "pending",
|
||||
"v": "待认证"
|
||||
},
|
||||
{
|
||||
"id": "rl_gs_inactive",
|
||||
"parentid": "rl_group_status",
|
||||
"k": "inactive",
|
||||
"v": "停用"
|
||||
},
|
||||
{
|
||||
"id": "rl_at_image",
|
||||
"parentid": "rl_asset_type",
|
||||
"k": "Image",
|
||||
"v": "图片"
|
||||
},
|
||||
{
|
||||
"id": "rl_at_video",
|
||||
"parentid": "rl_asset_type",
|
||||
"k": "Video",
|
||||
"v": "视频"
|
||||
},
|
||||
{
|
||||
"id": "rl_at_audio",
|
||||
"parentid": "rl_asset_type",
|
||||
"k": "Audio",
|
||||
"v": "音频"
|
||||
},
|
||||
{
|
||||
"id": "rl_as_active",
|
||||
"parentid": "rl_asset_status",
|
||||
"k": "Active",
|
||||
"v": "可用"
|
||||
},
|
||||
{
|
||||
"id": "rl_as_processing",
|
||||
"parentid": "rl_asset_status",
|
||||
"k": "Processing",
|
||||
"v": "处理中"
|
||||
},
|
||||
{
|
||||
"id": "rl_as_failed",
|
||||
"parentid": "rl_asset_status",
|
||||
"k": "Failed",
|
||||
"v": "失败"
|
||||
}
|
||||
]
|
||||
}
|
||||
72
json/rl_asset_group_list.json
Normal file
72
json/rl_asset_group_list.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"tblname": "rl_asset_group",
|
||||
"alias": "rl_asset_group_list",
|
||||
"title": "真人素材组合管理",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"create_time desc"
|
||||
],
|
||||
"logined_userorgid": "org_id",
|
||||
"confidential_fields": [
|
||||
"byted_token",
|
||||
"h5_link"
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"byted_token",
|
||||
"h5_link",
|
||||
"callback_url"
|
||||
],
|
||||
"alters": {
|
||||
"vendor": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "volcengine",
|
||||
"text": "火山方舟"
|
||||
},
|
||||
{
|
||||
"value": "kling",
|
||||
"text": "可灵"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"text": "其他"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "active",
|
||||
"text": "正常"
|
||||
},
|
||||
{
|
||||
"value": "pending",
|
||||
"text": "待认证"
|
||||
},
|
||||
{
|
||||
"value": "inactive",
|
||||
"text": "停用"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/rl_asset_group_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/rl_asset_group_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/rl_asset_group_delete.dspy')}}"
|
||||
},
|
||||
"subtables": [
|
||||
{
|
||||
"field": "group_id",
|
||||
"title": "素材资产",
|
||||
"url": "{{entire_url('../rl_asset_list')}}",
|
||||
"subtable": "rl_asset"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
79
json/rl_asset_list.json
Normal file
79
json/rl_asset_list.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"tblname": "rl_asset",
|
||||
"alias": "rl_asset_list",
|
||||
"title": "真人素材资产管理",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"create_time desc"
|
||||
],
|
||||
"logined_userorgid": "org_id",
|
||||
"confidential_fields": [
|
||||
"vendor_response"
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"vendor_response",
|
||||
"source_url"
|
||||
],
|
||||
"alters": {
|
||||
"vendor": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "volcengine",
|
||||
"text": "火山方舟"
|
||||
},
|
||||
{
|
||||
"value": "kling",
|
||||
"text": "可灵"
|
||||
},
|
||||
{
|
||||
"value": "other",
|
||||
"text": "其他"
|
||||
}
|
||||
]
|
||||
},
|
||||
"asset_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "Image",
|
||||
"text": "图片"
|
||||
},
|
||||
{
|
||||
"value": "Video",
|
||||
"text": "视频"
|
||||
},
|
||||
{
|
||||
"value": "Audio",
|
||||
"text": "音频"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "Active",
|
||||
"text": "可用"
|
||||
},
|
||||
{
|
||||
"value": "Processing",
|
||||
"text": "处理中"
|
||||
},
|
||||
{
|
||||
"value": "Failed",
|
||||
"text": "失败"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/rl_asset_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/rl_asset_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/rl_asset_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
171
models/rl_asset.json
Normal file
171
models/rl_asset.json
Normal file
@ -0,0 +1,171 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "rl_asset",
|
||||
"title": "真人人像素材资产",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "所属机构",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "group_id",
|
||||
"title": "素材组合ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "vendor",
|
||||
"title": "供应商",
|
||||
"type": "str",
|
||||
"length": 50,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "vendor_asset_id",
|
||||
"title": "供应商端资产ID",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "asset_type",
|
||||
"title": "素材类型",
|
||||
"type": "str",
|
||||
"length": 20,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "素材名称",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 20,
|
||||
"default": "Processing"
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"title": "素材URL",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "asset_uri",
|
||||
"title": "素材URI",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "project_name",
|
||||
"title": "项目名",
|
||||
"type": "str",
|
||||
"length": 100,
|
||||
"default": "default"
|
||||
},
|
||||
{
|
||||
"name": "source_url",
|
||||
"title": "上传源URL",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "vendor_response",
|
||||
"title": "供应商响应",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "create_time",
|
||||
"title": "创建时间",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "update_time",
|
||||
"title": "更新时间",
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_rl_asset_org",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_rl_asset_group",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"group_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_rl_asset_vendor_aid",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"vendor",
|
||||
"vendor_asset_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_rl_asset_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"status"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "group_id",
|
||||
"table": "rl_asset_group",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "vendor",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='rl_vendor'"
|
||||
},
|
||||
{
|
||||
"field": "asset_type",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='rl_asset_type'"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='rl_asset_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
151
models/rl_asset_group.json
Normal file
151
models/rl_asset_group.json
Normal file
@ -0,0 +1,151 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "rl_asset_group",
|
||||
"title": "真人人像素材组合",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "所属机构",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "vendor",
|
||||
"title": "供应商",
|
||||
"type": "str",
|
||||
"length": 50,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "vendor_group_id",
|
||||
"title": "供应商端组ID",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "组合名称",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "显示标题",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"title": "描述",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "group_type",
|
||||
"title": "组合类型",
|
||||
"type": "str",
|
||||
"length": 50,
|
||||
"default": "LivenessFace"
|
||||
},
|
||||
{
|
||||
"name": "project_name",
|
||||
"title": "项目名",
|
||||
"type": "str",
|
||||
"length": 100,
|
||||
"default": "default"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态",
|
||||
"type": "str",
|
||||
"length": 20,
|
||||
"default": "active"
|
||||
},
|
||||
{
|
||||
"name": "byted_token",
|
||||
"title": "认证Token",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "h5_link",
|
||||
"title": "H5认证链接",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "callback_url",
|
||||
"title": "回调URL",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "create_time",
|
||||
"title": "创建时间",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "update_time",
|
||||
"title": "更新时间",
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_rl_asset_group_org",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_rl_asset_group_vendor_gid",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"vendor",
|
||||
"vendor_group_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_rl_asset_group_vendor",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"vendor"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "vendor",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='rl_vendor'"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='rl_group_status'"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
pyproject.toml
Normal file
17
pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "reallife_asset"
|
||||
version = "1.0.0"
|
||||
description = "Real Person Portrait Asset Management Module - supports multiple vendors (Volcengine Ark, etc.)"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"sqlor",
|
||||
"bricks_for_python",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["reallife_asset*"]
|
||||
1
reallife_asset/__init__.py
Normal file
1
reallife_asset/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# reallife_asset module
|
||||
405
reallife_asset/init.py
Normal file
405
reallife_asset/init.py
Normal file
@ -0,0 +1,405 @@
|
||||
"""
|
||||
reallife_asset module - Real Person Portrait Asset Management.
|
||||
Supports multiple vendors (Volcengine Ark, etc.) for managing
|
||||
real person portrait asset groups and assets.
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from traceback import format_exc
|
||||
|
||||
from sqlor.dbpools import DBPools
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.log import debug, exception, error
|
||||
from appPublic.uniqueID import getID
|
||||
from appPublic.dictObject import DictObject
|
||||
|
||||
from .volcengine_client import get_vendor_client
|
||||
|
||||
MODULE_NAME = "reallife_asset"
|
||||
|
||||
|
||||
def _get_dbname():
|
||||
f = ServerEnv().get_module_dbname
|
||||
return f(MODULE_NAME)
|
||||
|
||||
|
||||
def _get_client(vendor, apikey, secretkey):
|
||||
"""Get vendor API client."""
|
||||
return get_vendor_client(vendor, apikey, secretkey)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Asset Group operations
|
||||
# ============================================================
|
||||
|
||||
async def rl_create_validate_session(org_id, vendor, callback_url,
|
||||
project_name="default",
|
||||
apikey=None, secretkey=None,
|
||||
user_id=None):
|
||||
"""Create H5 verification session for real person auth."""
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.create_visual_validate_session(callback_url, project_name)
|
||||
|
||||
if "error" in result:
|
||||
return {"success": False, "message": result.get("error", "API调用失败")}
|
||||
|
||||
byted_token = result.get("BytedToken", "")
|
||||
h5_link = result.get("H5Link", "")
|
||||
|
||||
# Save to local DB
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
gid = getID()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
await sor.I("rl_asset_group", {
|
||||
"id": gid,
|
||||
"org_id": org_id,
|
||||
"vendor": vendor,
|
||||
"name": f"待认证-{now}",
|
||||
"title": f"待认证-{now}",
|
||||
"group_type": "LivenessFace",
|
||||
"project_name": project_name,
|
||||
"status": "pending",
|
||||
"byted_token": byted_token,
|
||||
"h5_link": h5_link,
|
||||
"callback_url": callback_url,
|
||||
"created_by": user_id or "",
|
||||
"create_time": now,
|
||||
"update_time": now,
|
||||
})
|
||||
return {
|
||||
"success": True,
|
||||
"id": gid,
|
||||
"byted_token": byted_token,
|
||||
"h5_link": h5_link,
|
||||
}
|
||||
|
||||
|
||||
async def rl_check_validate_result(local_group_id, vendor,
|
||||
apikey=None, secretkey=None):
|
||||
"""Check real person validation result and get vendor Group ID."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset_group", {"id": local_group_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "本地记录不存在"}
|
||||
rec = recs[0]
|
||||
byted_token = rec.byted_token
|
||||
project_name = rec.project_name or "default"
|
||||
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.get_visual_validate_result(byted_token, project_name)
|
||||
|
||||
if "error" in result:
|
||||
return {"success": False, "message": result.get("error", "查询失败")}
|
||||
|
||||
vendor_group_id = result.get("GroupId", "")
|
||||
if not vendor_group_id:
|
||||
return {"success": False, "message": "尚未完成认证或认证失败"}
|
||||
|
||||
# Update local record
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.U("rl_asset_group", {
|
||||
"vendor_group_id": vendor_group_id,
|
||||
"status": "active",
|
||||
"name": f"已认证-{vendor_group_id}",
|
||||
"title": f"已认证-{vendor_group_id}",
|
||||
"update_time": now,
|
||||
}, {"id": local_group_id})
|
||||
|
||||
return {"success": True, "vendor_group_id": vendor_group_id}
|
||||
|
||||
|
||||
async def rl_create_asset(org_id, local_group_id, source_url,
|
||||
asset_type="Image", name="",
|
||||
vendor=None, apikey=None, secretkey=None,
|
||||
user_id=None):
|
||||
"""Upload asset to vendor and create local record."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset_group", {"id": local_group_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "素材组合不存在"}
|
||||
grp = recs[0]
|
||||
vendor = vendor or grp.vendor
|
||||
vendor_group_id = grp.vendor_group_id
|
||||
project_name = grp.project_name or "default"
|
||||
|
||||
if not vendor_group_id:
|
||||
return {"success": False, "message": "素材组合尚未完成真人认证"}
|
||||
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.create_asset(
|
||||
vendor_group_id, source_url, asset_type, name, project_name
|
||||
)
|
||||
|
||||
vendor_asset_id = result.get("Id", "")
|
||||
if not vendor_asset_id and "error" not in result:
|
||||
# Try nested result structure
|
||||
r = result.get("Result", {})
|
||||
vendor_asset_id = r.get("Id", "")
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
asset_id = getID()
|
||||
asset_uri = f"asset://{vendor_asset_id}" if vendor_asset_id else ""
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.I("rl_asset", {
|
||||
"id": asset_id,
|
||||
"org_id": org_id,
|
||||
"group_id": local_group_id,
|
||||
"vendor": vendor,
|
||||
"vendor_asset_id": vendor_asset_id,
|
||||
"asset_type": asset_type,
|
||||
"name": name or source_url.split("/")[-1][:50],
|
||||
"status": "Processing",
|
||||
"source_url": source_url,
|
||||
"asset_uri": asset_uri,
|
||||
"project_name": project_name,
|
||||
"vendor_response": json.dumps(result, ensure_ascii=False),
|
||||
"created_by": user_id or "",
|
||||
"create_time": now,
|
||||
"update_time": now,
|
||||
})
|
||||
|
||||
return {
|
||||
"success": "error" not in result,
|
||||
"id": asset_id,
|
||||
"vendor_asset_id": vendor_asset_id,
|
||||
"status": "Processing",
|
||||
"message": result.get("error", ""),
|
||||
}
|
||||
|
||||
|
||||
async def rl_sync_asset_status(asset_id, vendor=None,
|
||||
apikey=None, secretkey=None):
|
||||
"""Sync asset status from vendor."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset", {"id": asset_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "素材不存在"}
|
||||
rec = recs[0]
|
||||
vendor = vendor or rec.vendor
|
||||
vendor_asset_id = rec.vendor_asset_id
|
||||
project_name = rec.project_name or "default"
|
||||
|
||||
if not vendor_asset_id:
|
||||
return {"success": False, "message": "无供应商端资产ID"}
|
||||
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.get_asset(vendor_asset_id, project_name)
|
||||
|
||||
if "error" in result:
|
||||
return {"success": False, "message": result.get("error", "查询失败")}
|
||||
|
||||
# Extract status from result (may be nested under Result)
|
||||
status = result.get("Status", "")
|
||||
if not status:
|
||||
r = result.get("Result", {})
|
||||
status = r.get("Status", result.get("status", ""))
|
||||
url = result.get("URL", result.get("Result", {}).get("URL", ""))
|
||||
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
upd = {
|
||||
"status": status,
|
||||
"update_time": now,
|
||||
"vendor_response": json.dumps(result, ensure_ascii=False),
|
||||
}
|
||||
if url:
|
||||
upd["url"] = url
|
||||
await sor.U("rl_asset", upd, {"id": asset_id})
|
||||
|
||||
return {"success": True, "status": status, "url": url}
|
||||
|
||||
|
||||
async def rl_delete_asset(asset_id, vendor=None,
|
||||
apikey=None, secretkey=None):
|
||||
"""Delete asset from vendor and local DB."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset", {"id": asset_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "素材不存在"}
|
||||
rec = recs[0]
|
||||
vendor = vendor or rec.vendor
|
||||
vendor_asset_id = rec.vendor_asset_id
|
||||
project_name = rec.project_name or "default"
|
||||
|
||||
# Delete from vendor
|
||||
if vendor_asset_id:
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.delete_asset(vendor_asset_id, project_name)
|
||||
debug(f"vendor delete asset: {result}")
|
||||
|
||||
# Delete local
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D("rl_asset", {"id": asset_id})
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
async def rl_delete_group(local_group_id, vendor=None,
|
||||
apikey=None, secretkey=None):
|
||||
"""Delete asset group from vendor and local DB (cascade)."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset_group", {"id": local_group_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "素材组合不存在"}
|
||||
rec = recs[0]
|
||||
vendor = vendor or rec.vendor
|
||||
vendor_group_id = rec.vendor_group_id
|
||||
project_name = rec.project_name or "default"
|
||||
|
||||
# Delete from vendor
|
||||
if vendor_group_id:
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.delete_asset_group(vendor_group_id, project_name)
|
||||
debug(f"vendor delete group: {result}")
|
||||
|
||||
# Delete local (cascade)
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D("rl_asset", {"group_id": local_group_id})
|
||||
await sor.D("rl_asset_group", {"id": local_group_id})
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
async def rl_sync_group_from_vendor(org_id, vendor,
|
||||
apikey=None, secretkey=None,
|
||||
project_name="default"):
|
||||
"""Sync asset groups from vendor to local DB."""
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.list_asset_groups(project_name=project_name)
|
||||
|
||||
items = result.get("Items", result.get("Result", {}).get("Items", []))
|
||||
synced = 0
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
for item in items:
|
||||
vgid = item.get("Id", "")
|
||||
if not vgid:
|
||||
continue
|
||||
# Check if exists
|
||||
existing = await sor.R("rl_asset_group", {
|
||||
"vendor": vendor,
|
||||
"vendor_group_id": vgid,
|
||||
})
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if existing:
|
||||
await sor.U("rl_asset_group", {
|
||||
"name": item.get("Name", ""),
|
||||
"title": item.get("Title", item.get("Name", "")),
|
||||
"description": item.get("Description", ""),
|
||||
"update_time": now,
|
||||
}, {"id": existing[0].id})
|
||||
else:
|
||||
gid = getID()
|
||||
await sor.I("rl_asset_group", {
|
||||
"id": gid,
|
||||
"org_id": org_id,
|
||||
"vendor": vendor,
|
||||
"vendor_group_id": vgid,
|
||||
"name": item.get("Name", ""),
|
||||
"title": item.get("Title", item.get("Name", "")),
|
||||
"description": item.get("Description", ""),
|
||||
"group_type": item.get("GroupType", "LivenessFace"),
|
||||
"project_name": item.get("ProjectName", project_name),
|
||||
"status": "active",
|
||||
"create_time": item.get("CreateTime", now),
|
||||
"update_time": now,
|
||||
})
|
||||
synced += 1
|
||||
|
||||
return {"success": True, "synced": synced}
|
||||
|
||||
|
||||
async def rl_sync_assets_from_vendor(org_id, local_group_id,
|
||||
vendor=None, apikey=None, secretkey=None):
|
||||
"""Sync assets for a group from vendor to local DB."""
|
||||
dbname = _get_dbname()
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset_group", {"id": local_group_id})
|
||||
if not recs:
|
||||
return {"success": False, "message": "素材组合不存在"}
|
||||
grp = recs[0]
|
||||
vendor = vendor or grp.vendor
|
||||
vendor_group_id = grp.vendor_group_id
|
||||
project_name = grp.project_name or "default"
|
||||
|
||||
if not vendor_group_id:
|
||||
return {"success": False, "message": "无供应商端组合ID"}
|
||||
|
||||
client = _get_client(vendor, apikey, secretkey)
|
||||
result = client.list_assets(group_ids=[vendor_group_id])
|
||||
|
||||
items = result.get("Items", result.get("Result", {}).get("Items", []))
|
||||
synced = 0
|
||||
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
for item in items:
|
||||
vaid = item.get("Id", "")
|
||||
if not vaid:
|
||||
continue
|
||||
existing = await sor.R("rl_asset", {
|
||||
"vendor": vendor,
|
||||
"vendor_asset_id": vaid,
|
||||
})
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if existing:
|
||||
await sor.U("rl_asset", {
|
||||
"status": item.get("Status", ""),
|
||||
"url": item.get("URL", ""),
|
||||
"name": item.get("Name", existing[0].name),
|
||||
"update_time": now,
|
||||
}, {"id": existing[0].id})
|
||||
else:
|
||||
aid = getID()
|
||||
await sor.I("rl_asset", {
|
||||
"id": aid,
|
||||
"org_id": org_id,
|
||||
"group_id": local_group_id,
|
||||
"vendor": vendor,
|
||||
"vendor_asset_id": vaid,
|
||||
"asset_type": item.get("AssetType", "Image"),
|
||||
"name": item.get("Name", ""),
|
||||
"status": item.get("Status", "Processing"),
|
||||
"url": item.get("URL", ""),
|
||||
"asset_uri": f"asset://{vaid}",
|
||||
"project_name": item.get("ProjectName", project_name),
|
||||
"create_time": item.get("CreateTime", now),
|
||||
"update_time": now,
|
||||
})
|
||||
synced += 1
|
||||
|
||||
return {"success": True, "synced": synced}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Module loader
|
||||
# ============================================================
|
||||
|
||||
def load_reallife_asset():
|
||||
"""Register all functions with ServerEnv."""
|
||||
g = ServerEnv()
|
||||
g.rl_create_validate_session = rl_create_validate_session
|
||||
g.rl_check_validate_result = rl_check_validate_result
|
||||
g.rl_create_asset = rl_create_asset
|
||||
g.rl_sync_asset_status = rl_sync_asset_status
|
||||
g.rl_delete_asset = rl_delete_asset
|
||||
g.rl_delete_group = rl_delete_group
|
||||
g.rl_sync_group_from_vendor = rl_sync_group_from_vendor
|
||||
g.rl_sync_assets_from_vendor = rl_sync_assets_from_vendor
|
||||
return True
|
||||
269
reallife_asset/volcengine_client.py
Normal file
269
reallife_asset/volcengine_client.py
Normal file
@ -0,0 +1,269 @@
|
||||
"""
|
||||
Volcengine Ark API Client for Real Person Portrait Asset Management.
|
||||
Implements AK/SK HMAC-SHA256 signing (Volcengine V4 signature).
|
||||
Uses only Python stdlib (no external dependencies).
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
import hmac
|
||||
import datetime
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SERVICE = "ark"
|
||||
REGION = "cn-beijing"
|
||||
VERSION = "2024-01-01"
|
||||
HOST = "open.volcengineapi.com"
|
||||
BASE_URL = f"https://{HOST}"
|
||||
|
||||
|
||||
def _sign(key, msg):
|
||||
"""HMAC-SHA256 sign."""
|
||||
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
||||
|
||||
|
||||
def _get_signature_key(secret_key, date_stamp, region, service):
|
||||
"""Derive the signing key."""
|
||||
k_date = _sign(secret_key.encode("utf-8"), date_stamp)
|
||||
k_region = _sign(k_date, region)
|
||||
k_service = _sign(k_region, service)
|
||||
k_signing = _sign(k_service, "request")
|
||||
return k_signing
|
||||
|
||||
|
||||
class VolcengineArkClient:
|
||||
"""Client for Volcengine Ark API (Real Person Portrait Assets)."""
|
||||
|
||||
def __init__(self, access_key, secret_key, region=REGION):
|
||||
self.access_key = access_key
|
||||
self.secret_key = secret_key
|
||||
self.region = region
|
||||
|
||||
def _build_signed_request(self, action, body_dict, method="POST"):
|
||||
"""Build a signed urllib Request for the given API action."""
|
||||
now = datetime.datetime.utcnow()
|
||||
date_stamp = now.strftime("%Y%m%d")
|
||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
||||
|
||||
# Query string with Action and Version
|
||||
query_params = {
|
||||
"Action": action,
|
||||
"Version": VERSION,
|
||||
}
|
||||
canonical_querystring = urllib.parse.urlencode(sorted(query_params.items()))
|
||||
|
||||
# Headers
|
||||
content_type = "application/json"
|
||||
payload = json.dumps(body_dict, ensure_ascii=False)
|
||||
payload_hash = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
||||
|
||||
headers_to_sign = {
|
||||
"host": HOST,
|
||||
"x-date": amz_date,
|
||||
"x-content-sha256": payload_hash,
|
||||
"content-type": content_type,
|
||||
}
|
||||
signed_headers = ";".join(sorted(headers_to_sign.keys()))
|
||||
canonical_headers = "".join(
|
||||
f"{k}:{v}\n" for k, v in sorted(headers_to_sign.items())
|
||||
)
|
||||
|
||||
# Canonical request
|
||||
canonical_request = "\n".join([
|
||||
method,
|
||||
"/",
|
||||
canonical_querystring,
|
||||
canonical_headers,
|
||||
signed_headers,
|
||||
payload_hash,
|
||||
])
|
||||
|
||||
# String to sign
|
||||
credential_scope = f"{date_stamp}/{self.region}/{SERVICE}/request"
|
||||
string_to_sign = "\n".join([
|
||||
"HMAC-SHA256",
|
||||
amz_date,
|
||||
credential_scope,
|
||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
||||
])
|
||||
|
||||
# Signing key and signature
|
||||
signing_key = _get_signature_key(
|
||||
self.secret_key, date_stamp, self.region, SERVICE
|
||||
)
|
||||
signature = hmac.new(
|
||||
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Authorization header
|
||||
authorization = (
|
||||
f"HMAC-SHA256 "
|
||||
f"Credential={self.access_key}/{credential_scope}, "
|
||||
f"SignedHeaders={signed_headers}, "
|
||||
f"Signature={signature}"
|
||||
)
|
||||
|
||||
# Build request
|
||||
url = f"{BASE_URL}/?{canonical_querystring}"
|
||||
req_headers = {
|
||||
"Host": HOST,
|
||||
"X-Date": amz_date,
|
||||
"X-Content-Sha256": payload_hash,
|
||||
"Content-Type": content_type,
|
||||
"Authorization": authorization,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=payload.encode("utf-8"),
|
||||
headers=req_headers,
|
||||
method=method,
|
||||
)
|
||||
return req
|
||||
|
||||
def _call(self, action, body_dict):
|
||||
"""Execute a signed API call and return parsed JSON response."""
|
||||
req = self._build_signed_request(action, body_dict)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = json.loads(resp.read().decode("utf-8"))
|
||||
return data
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
logger.error("Volcengine API error: %s %s -> %s", action, e.code, body)
|
||||
try:
|
||||
return json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
return {"error": body, "code": e.code}
|
||||
except Exception as e:
|
||||
logger.error("Volcengine API exception: %s -> %s", action, e)
|
||||
return {"error": str(e)}
|
||||
|
||||
# ---- Real Person Portrait Asset APIs ----
|
||||
|
||||
def create_visual_validate_session(self, callback_url, project_name="default"):
|
||||
"""Create H5 verification page link for real person authentication."""
|
||||
return self._call("CreateVisualValidateSession", {
|
||||
"CallbackURL": callback_url,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
def get_visual_validate_result(self, byted_token, project_name="default"):
|
||||
"""Get Asset Group ID after real person authentication."""
|
||||
return self._call("GetVisualValidateResult", {
|
||||
"BytedToken": byted_token,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
def create_asset(self, group_id, url, asset_type="Image",
|
||||
name="", project_name="default"):
|
||||
"""Upload a new asset (async - returns asset ID immediately)."""
|
||||
body = {
|
||||
"GroupId": group_id,
|
||||
"URL": url,
|
||||
"AssetType": asset_type,
|
||||
"ProjectName": project_name,
|
||||
}
|
||||
if name:
|
||||
body["Name"] = name
|
||||
return self._call("CreateAsset", body)
|
||||
|
||||
def get_asset(self, asset_id, project_name="default"):
|
||||
"""Get single asset info including status."""
|
||||
return self._call("GetAsset", {
|
||||
"Id": asset_id,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
def list_assets(self, group_ids=None, statuses=None, name=None,
|
||||
group_type="LivenessFace", page_number=1, page_size=10,
|
||||
sort_by="CreateTime", sort_order="Desc"):
|
||||
"""List assets with optional filters."""
|
||||
filter_obj = {"GroupType": group_type}
|
||||
if group_ids:
|
||||
filter_obj["GroupIds"] = group_ids
|
||||
if statuses:
|
||||
filter_obj["Statuses"] = statuses
|
||||
if name:
|
||||
filter_obj["Name"] = name
|
||||
return self._call("ListAssets", {
|
||||
"Filter": filter_obj,
|
||||
"PageNumber": page_number,
|
||||
"PageSize": page_size,
|
||||
"SortBy": sort_by,
|
||||
"SortOrder": sort_order,
|
||||
})
|
||||
|
||||
def list_asset_groups(self, name=None, group_ids=None,
|
||||
group_type="LivenessFace",
|
||||
page_number=1, page_size=10):
|
||||
"""List asset groups with optional filters."""
|
||||
filter_obj = {"GroupType": group_type}
|
||||
if name:
|
||||
filter_obj["Name"] = name
|
||||
if group_ids:
|
||||
filter_obj["GroupIds"] = group_ids
|
||||
return self._call("ListAssetGroups", {
|
||||
"Filter": filter_obj,
|
||||
"PageNumber": page_number,
|
||||
"PageSize": page_size,
|
||||
})
|
||||
|
||||
def get_asset_group(self, group_id, project_name="default"):
|
||||
"""Get single asset group info."""
|
||||
return self._call("GetAssetGroup", {
|
||||
"Id": group_id,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
def update_asset(self, asset_id, name=None, project_name="default"):
|
||||
"""Update asset info (e.g. name)."""
|
||||
body = {"Id": asset_id, "ProjectName": project_name}
|
||||
if name is not None:
|
||||
body["Name"] = name
|
||||
return self._call("UpdateAsset", body)
|
||||
|
||||
def update_asset_group(self, group_id, name=None, title=None,
|
||||
description=None, project_name="default"):
|
||||
"""Update asset group info."""
|
||||
body = {"Id": group_id, "ProjectName": project_name}
|
||||
if name is not None:
|
||||
body["Name"] = name
|
||||
if title is not None:
|
||||
body["Title"] = title
|
||||
if description is not None:
|
||||
body["Description"] = description
|
||||
return self._call("UpdateAssetGroup", body)
|
||||
|
||||
def delete_asset(self, asset_id, project_name="default"):
|
||||
"""Delete a single asset."""
|
||||
return self._call("DeleteAsset", {
|
||||
"Id": asset_id,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
def delete_asset_group(self, group_id, project_name="default"):
|
||||
"""Delete an asset group."""
|
||||
return self._call("DeleteAssetGroup", {
|
||||
"Id": group_id,
|
||||
"ProjectName": project_name,
|
||||
})
|
||||
|
||||
|
||||
# ---- Provider Registry (extensible for future vendors) ----
|
||||
|
||||
_PROVIDERS = {
|
||||
"volcengine": VolcengineArkClient,
|
||||
}
|
||||
|
||||
|
||||
def get_vendor_client(vendor, access_key, secret_key, **kwargs):
|
||||
"""Factory: get API client for a given vendor."""
|
||||
cls = _PROVIDERS.get(vendor)
|
||||
if cls is None:
|
||||
raise ValueError(f"Unsupported vendor: {vendor}. Available: {list(_PROVIDERS.keys())}")
|
||||
return cls(access_key, secret_key, **kwargs)
|
||||
62
scripts/load_path.py
Normal file
62
scripts/load_path.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RBAC permission registration for reallife_asset module."""
|
||||
import os, sys, subprocess
|
||||
|
||||
# Find Sage root
|
||||
home = os.path.expanduser("~")
|
||||
sage_root = ""
|
||||
for candidate in [
|
||||
os.path.join(home, "repos/sage"),
|
||||
os.path.join(home, "sage"),
|
||||
]:
|
||||
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
||||
sage_root = candidate
|
||||
break
|
||||
|
||||
if not sage_root:
|
||||
print("ERROR: Cannot find Sage root")
|
||||
sys.exit(1)
|
||||
|
||||
python = os.path.join(sage_root, "py3/bin/python")
|
||||
set_perm = os.path.join(sage_root, "set_role_perm.py")
|
||||
|
||||
# Permission definitions
|
||||
paths_any = [] # No login required
|
||||
paths_logined = [
|
||||
"/reallife_asset",
|
||||
"/reallife_asset/index.ui",
|
||||
"/reallife_asset/group_manage.ui",
|
||||
"/reallife_asset/asset_manage.ui",
|
||||
"/reallife_asset/create_validate.ui",
|
||||
"/reallife_asset/upload_asset.ui",
|
||||
"/reallife_asset/sync_groups.ui",
|
||||
"/reallife_asset/rl_asset_group_list",
|
||||
"/reallife_asset/rl_asset_group_list/index.ui",
|
||||
"/reallife_asset/rl_asset_list",
|
||||
"/reallife_asset/rl_asset_list/index.ui",
|
||||
"/reallife_asset/api/rl_asset_group_create.dspy",
|
||||
"/reallife_asset/api/rl_asset_group_update.dspy",
|
||||
"/reallife_asset/api/rl_asset_group_delete.dspy",
|
||||
"/reallife_asset/api/rl_asset_create.dspy",
|
||||
"/reallife_asset/api/rl_asset_update.dspy",
|
||||
"/reallife_asset/api/rl_asset_delete.dspy",
|
||||
"/reallife_asset/api/sync_asset_status.dspy",
|
||||
"/reallife_asset/api/check_validate.dspy",
|
||||
"/reallife_asset/api/sync_from_vendor.dspy",
|
||||
"/reallife_asset/api/sync_assets.dspy",
|
||||
"/reallife_asset/api/get_rl_asset_group_list.dspy",
|
||||
"/reallife_asset/api/get_rl_asset_list.dspy",
|
||||
]
|
||||
|
||||
def run_set_perm(role, path):
|
||||
cmd = [python, set_perm, role, path]
|
||||
print(f" {role:12s} {path}")
|
||||
subprocess.run(cmd, cwd=sage_root)
|
||||
|
||||
print("Registering RBAC permissions for reallife_asset...")
|
||||
for p in paths_any:
|
||||
run_set_perm("any", p)
|
||||
for p in paths_logined:
|
||||
run_set_perm("logined", p)
|
||||
|
||||
print("Done.")
|
||||
335
scripts/uapi_volcengine_ark.sql
Normal file
335
scripts/uapi_volcengine_ark.sql
Normal file
@ -0,0 +1,335 @@
|
||||
-- ============================================================
|
||||
-- uapi SQL Configuration for Volcengine Ark Real Person Portrait APIs
|
||||
-- Execute against the Sage database to register these APIs
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Register Volcengine Ark as an external application (upapp)
|
||||
-- The AK/SK signing is handled by the dynamic_func
|
||||
INSERT INTO upapp (id, name, description, ownerid, apisetid, secretkey, baseurl, myappid, dynamic_func, auth_apiname) VALUES (
|
||||
'volcengine_ark',
|
||||
'volcengine_ark',
|
||||
'火山方舟 - 真人人像素材管理API (AK/SK签名)',
|
||||
'0',
|
||||
'',
|
||||
'',
|
||||
'https://open.volcengineapi.com',
|
||||
'',
|
||||
'volcengine_ark_sign',
|
||||
NULL
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description), baseurl=VALUES(baseurl);
|
||||
|
||||
-- 2. Register API Key storage (upappkey)
|
||||
-- Replace <YOUR_AK> and <YOUR_SK> with actual Volcengine credentials
|
||||
-- The apikey = Access Key, secretkey = Secret Key
|
||||
-- INSERT INTO upappkey (id, upappid, ownerid, myappid, apikey, secretkey) VALUES (
|
||||
-- 'volcengine_ark_key_1',
|
||||
-- 'volcengine_ark',
|
||||
-- '<owner_user_id>',
|
||||
-- '',
|
||||
-- password_encode('<YOUR_ACCESS_KEY>'),
|
||||
-- password_encode('<YOUR_SECRET_KEY>')
|
||||
-- );
|
||||
|
||||
-- 3. Input/Output definitions (uapiio)
|
||||
|
||||
-- CreateVisualValidateSession IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_cvvs_io',
|
||||
'创建真人认证会话',
|
||||
'拉起H5真人认证页',
|
||||
'{"CallbackURL": {"type": "string", "required": true, "description": "回调URL"}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- GetVisualValidateResult IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_gvvr_io',
|
||||
'获取真人认证结果',
|
||||
'查询认证创建的Asset Group ID',
|
||||
'{"BytedToken": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- CreateAsset IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_create_asset_io',
|
||||
'创建素材资产',
|
||||
'上传素材到真人认证组合',
|
||||
'{"GroupId": {"type": "string", "required": true}, "URL": {"type": "string", "required": true}, "AssetType": {"type": "string", "required": true, "options": ["Image","Video","Audio"]}, "Name": {"type": "string", "required": false}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- GetAsset IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_get_asset_io',
|
||||
'获取素材信息',
|
||||
'查询单个素材资产状态和信息',
|
||||
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- ListAssets IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_list_assets_io',
|
||||
'查询素材列表',
|
||||
'分页查询素材资产列表',
|
||||
'{"Filter": {"type": "object", "required": false}, "PageNumber": {"type": "integer", "default": 1}, "PageSize": {"type": "integer", "default": 10}, "SortBy": {"type": "string", "default": "CreateTime"}, "SortOrder": {"type": "string", "default": "Desc"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- ListAssetGroups IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_list_groups_io',
|
||||
'查询素材组合列表',
|
||||
'分页查询素材组合列表',
|
||||
'{"Filter": {"type": "object", "required": false}, "PageNumber": {"type": "integer", "default": 1}, "PageSize": {"type": "integer", "default": 10}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- GetAssetGroup IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_get_group_io',
|
||||
'获取素材组合信息',
|
||||
'查询单个素材组合',
|
||||
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "required": false, "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- UpdateAsset IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_update_asset_io',
|
||||
'更新素材信息',
|
||||
'更新素材资产名称等',
|
||||
'{"Id": {"type": "string", "required": true}, "Name": {"type": "string"}, "ProjectName": {"type": "string", "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- UpdateAssetGroup IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_update_group_io',
|
||||
'更新素材组合',
|
||||
'更新素材组合信息',
|
||||
'{"Id": {"type": "string", "required": true}, "Name": {"type": "string"}, "Title": {"type": "string"}, "Description": {"type": "string"}, "ProjectName": {"type": "string", "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- DeleteAsset IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_delete_asset_io',
|
||||
'删除素材',
|
||||
'删除单个素材资产',
|
||||
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
-- DeleteAssetGroup IO
|
||||
INSERT INTO uapiio (id, name, description, input_fields) VALUES (
|
||||
'volc_delete_group_io',
|
||||
'删除素材组合',
|
||||
'删除指定素材组合',
|
||||
'{"Id": {"type": "string", "required": true}, "ProjectName": {"type": "string", "default": "default"}}'
|
||||
) ON DUPLICATE KEY UPDATE description=VALUES(description);
|
||||
|
||||
|
||||
-- 4. API definitions (uapi)
|
||||
-- NOTE: All APIs use POST method with Action/Version in query string
|
||||
-- The dynamic_func 'volcengine_ark_sign' handles AK/SK HMAC signing
|
||||
|
||||
-- CreateVisualValidateSession
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_cvvs',
|
||||
'volcengine_ark',
|
||||
'createVisualValidateSession',
|
||||
'创建真人认证会话',
|
||||
'拉起H5真人认证页,返回H5Link和BytedToken',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=CreateVisualValidateSession&Version=2024-01-01',
|
||||
'POST',
|
||||
'BytedToken',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_cvvs_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- GetVisualValidateResult
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_gvvr',
|
||||
'volcengine_ark',
|
||||
'getVisualValidateResult',
|
||||
'获取真人认证结果',
|
||||
'获取认证创建的Asset Group ID',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=GetVisualValidateResult&Version=2024-01-01',
|
||||
'POST',
|
||||
'GroupId',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_gvvr_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- CreateAsset
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_create_asset',
|
||||
'volcengine_ark',
|
||||
'createAsset',
|
||||
'创建素材资产',
|
||||
'上传素材到真人认证组合(异步)',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=CreateAsset&Version=2024-01-01',
|
||||
'POST',
|
||||
'Id',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_create_asset_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- GetAsset
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_get_asset',
|
||||
'volcengine_ark',
|
||||
'getAsset',
|
||||
'获取素材信息',
|
||||
'查询单个素材资产',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=GetAsset&Version=2024-01-01',
|
||||
'POST',
|
||||
'Status',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_get_asset_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- ListAssets
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_list_assets',
|
||||
'volcengine_ark',
|
||||
'listAssets',
|
||||
'查询素材列表',
|
||||
'分页查询素材资产',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=ListAssets&Version=2024-01-01',
|
||||
'POST',
|
||||
'TotalCount',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_list_assets_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- ListAssetGroups
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_list_groups',
|
||||
'volcengine_ark',
|
||||
'listAssetGroups',
|
||||
'查询素材组合列表',
|
||||
'分页查询素材组合',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=ListAssetGroups&Version=2024-01-01',
|
||||
'POST',
|
||||
'TotalCount',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_list_groups_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- GetAssetGroup
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_get_group',
|
||||
'volcengine_ark',
|
||||
'getAssetGroup',
|
||||
'获取素材组合信息',
|
||||
'查询单个素材组合',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=GetAssetGroup&Version=2024-01-01',
|
||||
'POST',
|
||||
'Id',
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_get_group_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- UpdateAsset
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_update_asset',
|
||||
'volcengine_ark',
|
||||
'updateAsset',
|
||||
'更新素材信息',
|
||||
'更新素材名称等',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=UpdateAsset&Version=2024-01-01',
|
||||
'POST',
|
||||
NULL,
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_update_asset_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- UpdateAssetGroup
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_update_group',
|
||||
'volcengine_ark',
|
||||
'updateAssetGroup',
|
||||
'更新素材组合',
|
||||
'更新素材组合信息',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=UpdateAssetGroup&Version=2024-01-01',
|
||||
'POST',
|
||||
NULL,
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_update_group_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- DeleteAsset
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_delete_asset',
|
||||
'volcengine_ark',
|
||||
'deleteAsset',
|
||||
'删除素材',
|
||||
'删除单个素材资产',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=DeleteAsset&Version=2024-01-01',
|
||||
'POST',
|
||||
NULL,
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_delete_asset_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
|
||||
-- DeleteAssetGroup
|
||||
INSERT INTO uapi (id, upappid, name, title, description, need_auth, stream, path, httpmethod, chunk_match, headers, params, data, response, ioid) VALUES (
|
||||
'volc_delete_group',
|
||||
'volcengine_ark',
|
||||
'deleteAssetGroup',
|
||||
'删除素材组合',
|
||||
'删除指定素材组合',
|
||||
'0',
|
||||
'false',
|
||||
'/?Action=DeleteAssetGroup&Version=2024-01-01',
|
||||
'POST',
|
||||
NULL,
|
||||
'{"Content-Type":"application/json","Host":"open.volcengineapi.com"}',
|
||||
NULL,
|
||||
'{{jsondata}}',
|
||||
'{{response}}',
|
||||
'volc_delete_group_io'
|
||||
) ON DUPLICATE KEY UPDATE title=VALUES(title), path=VALUES(path);
|
||||
20
wwwroot/api/check_validate.dspy
Normal file
20
wwwroot/api/check_validate.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
import json
|
||||
|
||||
group_id = params_kw.get('group_id', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if not group_id:
|
||||
result = {"success": False, "message": "group_id 不能为空"}
|
||||
elif not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key"}
|
||||
else:
|
||||
from sqlor.dbpools import DBPools
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
recs = await sor.R("rl_asset_group", {"id": group_id})
|
||||
vendor = recs[0].vendor if recs else "volcengine"
|
||||
result = await rl_check_validate_result(group_id, vendor, apikey=apikey, secretkey=secretkey)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
14
wwwroot/api/get_rl_asset_group_list.dspy
Normal file
14
wwwroot/api/get_rl_asset_group_list.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ns = dict(params_kw)
|
||||
ns['org_id'] = org_id
|
||||
recs = await sor.R('rl_asset_group', ns)
|
||||
total = len(recs)
|
||||
data = [dict(r) for r in recs]
|
||||
|
||||
ret = json.dumps({"data": data, "total": total, "status": "ok"}, ensure_ascii=False)
|
||||
14
wwwroot/api/get_rl_asset_list.dspy
Normal file
14
wwwroot/api/get_rl_asset_list.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ns = dict(params_kw)
|
||||
ns['org_id'] = org_id
|
||||
recs = await sor.R('rl_asset', ns)
|
||||
total = len(recs)
|
||||
data = [dict(r) for r in recs]
|
||||
|
||||
ret = json.dumps({"data": data, "total": total, "status": "ok"}, ensure_ascii=False)
|
||||
25
wwwroot/api/rl_asset_create.dspy
Normal file
25
wwwroot/api/rl_asset_create.dspy
Normal file
@ -0,0 +1,25 @@
|
||||
import json
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
user_id = await get_user()
|
||||
group_id = params_kw.get('group_id', '')
|
||||
source_url = params_kw.get('source_url', '')
|
||||
asset_type = params_kw.get('asset_type', 'Image')
|
||||
name = params_kw.get('name', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if not group_id:
|
||||
result = {"success": False, "message": "请选择素材组合"}
|
||||
elif not source_url:
|
||||
result = {"success": False, "message": "请提供素材URL"}
|
||||
elif not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key"}
|
||||
else:
|
||||
result = await rl_create_asset(
|
||||
org_id, group_id, source_url,
|
||||
asset_type=asset_type, name=name,
|
||||
apikey=apikey, secretkey=secretkey, user_id=user_id
|
||||
)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
18
wwwroot/api/rl_asset_delete.dspy
Normal file
18
wwwroot/api/rl_asset_delete.dspy
Normal file
@ -0,0 +1,18 @@
|
||||
import json
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
rid = params_kw.get('id', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if apikey and secretkey:
|
||||
result = await rl_delete_asset(rid, apikey=apikey, secretkey=secretkey)
|
||||
else:
|
||||
from sqlor.dbpools import DBPools
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D("rl_asset", {"id": rid, "org_id": org_id})
|
||||
result = {"success": True, "message": "本地删除成功"}
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
23
wwwroot/api/rl_asset_group_create.dspy
Normal file
23
wwwroot/api/rl_asset_group_create.dspy
Normal file
@ -0,0 +1,23 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
from datetime import datetime
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
user_id = await get_user()
|
||||
vendor = params_kw.get('vendor', 'volcengine')
|
||||
callback_url = params_kw.get('callback_url', '')
|
||||
project_name = params_kw.get('project_name', 'default')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if not callback_url:
|
||||
result = {"success": False, "message": "callback_url 不能为空"}
|
||||
elif not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key (apikey/secretkey)"}
|
||||
else:
|
||||
result = await rl_create_validate_session(
|
||||
org_id, vendor, callback_url, project_name,
|
||||
apikey=apikey, secretkey=secretkey, user_id=user_id
|
||||
)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
20
wwwroot/api/rl_asset_group_delete.dspy
Normal file
20
wwwroot/api/rl_asset_group_delete.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
import json
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
rid = params_kw.get('id', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if apikey and secretkey:
|
||||
result = await rl_delete_group(rid, apikey=apikey, secretkey=secretkey)
|
||||
else:
|
||||
# Local-only delete
|
||||
from sqlor.dbpools import DBPools
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
await sor.D("rl_asset", {"group_id": rid})
|
||||
await sor.D("rl_asset_group", {"id": rid, "org_id": org_id})
|
||||
result = {"success": True, "message": "本地删除成功"}
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
20
wwwroot/api/rl_asset_group_update.dspy
Normal file
20
wwwroot/api/rl_asset_group_update.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
rid = params_kw.get('id', '')
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
upd = {"update_time": now}
|
||||
for f in ['name', 'title', 'description', 'status']:
|
||||
v = params_kw.get(f)
|
||||
if v is not None:
|
||||
upd[f] = v
|
||||
await sor.U("rl_asset_group", upd, {"id": rid, "org_id": org_id})
|
||||
|
||||
result = {"success": True, "message": "更新成功"}
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
20
wwwroot/api/rl_asset_update.dspy
Normal file
20
wwwroot/api/rl_asset_update.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
rid = params_kw.get('id', '')
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
dbname = get_module_dbname('reallife_asset')
|
||||
db = DBPools()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
upd = {"update_time": now}
|
||||
for f in ['name', 'status']:
|
||||
v = params_kw.get(f)
|
||||
if v is not None:
|
||||
upd[f] = v
|
||||
await sor.U("rl_asset", upd, {"id": rid, "org_id": org_id})
|
||||
|
||||
result = {"success": True, "message": "更新成功"}
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
14
wwwroot/api/sync_asset_status.dspy
Normal file
14
wwwroot/api/sync_asset_status.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
|
||||
asset_id = params_kw.get('asset_id', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if not asset_id:
|
||||
result = {"success": False, "message": "asset_id 不能为空"}
|
||||
elif not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key"}
|
||||
else:
|
||||
result = await rl_sync_asset_status(asset_id, apikey=apikey, secretkey=secretkey)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
17
wwwroot/api/sync_assets.dspy
Normal file
17
wwwroot/api/sync_assets.dspy
Normal file
@ -0,0 +1,17 @@
|
||||
import json
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
group_id = params_kw.get('group_id', '')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
|
||||
if not group_id:
|
||||
result = {"success": False, "message": "group_id 不能为空"}
|
||||
elif not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key"}
|
||||
else:
|
||||
result = await rl_sync_assets_from_vendor(
|
||||
org_id, group_id, apikey=apikey, secretkey=secretkey
|
||||
)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
17
wwwroot/api/sync_from_vendor.dspy
Normal file
17
wwwroot/api/sync_from_vendor.dspy
Normal file
@ -0,0 +1,17 @@
|
||||
import json
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
vendor = params_kw.get('vendor', 'volcengine')
|
||||
apikey = params_kw.get('apikey', '')
|
||||
secretkey = params_kw.get('secretkey', '')
|
||||
project_name = params_kw.get('project_name', 'default')
|
||||
|
||||
if not apikey or not secretkey:
|
||||
result = {"success": False, "message": "请提供供应商 API Key"}
|
||||
else:
|
||||
result = await rl_sync_group_from_vendor(
|
||||
org_id, vendor, apikey=apikey, secretkey=secretkey,
|
||||
project_name=project_name
|
||||
)
|
||||
|
||||
ret = json.dumps(result, ensure_ascii=False)
|
||||
55
wwwroot/asset_manage.ui
Normal file
55
wwwroot/asset_manage.ui
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"padding": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "素材资产管理",
|
||||
"fontSize": "20px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"gap": "12px",
|
||||
"marginBottom": "16px",
|
||||
"alignItems": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"label": "上传素材",
|
||||
"bgcolor": "#1890ff",
|
||||
"color": "#fff"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.rl_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('upload_asset.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "DataViewer",
|
||||
"options": {
|
||||
"data_url": "{{entire_url('api/get_rl_asset_list.dspy')}}",
|
||||
"crud_url": "{{entire_url('rl_asset_list')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
66
wwwroot/create_validate.ui
Normal file
66
wwwroot/create_validate.ui
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"padding": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "创建真人认证",
|
||||
"fontSize": "20px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "validate_form",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/rl_asset_group_create.dspy')}}",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "vendor",
|
||||
"label": "供应商",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "volcengine",
|
||||
"text": "火山方舟"
|
||||
},
|
||||
{
|
||||
"value": "kling",
|
||||
"text": "可灵"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "callback_url",
|
||||
"label": "回调URL",
|
||||
"type": "text",
|
||||
"placeholder": "https://your-domain.com/callback"
|
||||
},
|
||||
{
|
||||
"name": "project_name",
|
||||
"label": "项目名",
|
||||
"type": "text",
|
||||
"default": "default"
|
||||
},
|
||||
{
|
||||
"name": "apikey",
|
||||
"label": "Access Key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "secretkey",
|
||||
"label": "Secret Key",
|
||||
"type": "password"
|
||||
}
|
||||
],
|
||||
"submit_label": "生成认证链接"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
75
wwwroot/group_manage.ui
Normal file
75
wwwroot/group_manage.ui
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"padding": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "素材组合管理",
|
||||
"fontSize": "20px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "HBox",
|
||||
"options": {
|
||||
"gap": "12px",
|
||||
"marginBottom": "16px",
|
||||
"alignItems": "center"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"label": "创建真人认证",
|
||||
"bgcolor": "#1890ff",
|
||||
"color": "#fff"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.rl_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('create_validate.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"label": "从供应商同步",
|
||||
"bgcolor": "#52c41a",
|
||||
"color": "#fff"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.rl_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('sync_groups.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "DataViewer",
|
||||
"options": {
|
||||
"data_url": "{{entire_url('api/get_rl_asset_group_list.dspy')}}",
|
||||
"crud_url": "{{entire_url('rl_asset_group_list')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
117
wwwroot/index.ui
Normal file
117
wwwroot/index.ui
Normal file
@ -0,0 +1,117 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "真人人像素材管理",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "20px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "280px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"bgcolor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer",
|
||||
"borderRadius": "8px",
|
||||
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.rl_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('group_manage.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "📁 素材组合管理",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "创建真人认证、管理素材组合(Asset Group)",
|
||||
"fontSize": "14px",
|
||||
"color": "#666"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"bgcolor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer",
|
||||
"borderRadius": "8px",
|
||||
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.rl_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('asset_manage.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "🖼️ 素材资产管理",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "上传/查看/同步真人人像素材(Asset)",
|
||||
"fontSize": "14px",
|
||||
"color": "#666"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "rl_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
56
wwwroot/sync_groups.ui
Normal file
56
wwwroot/sync_groups.ui
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"padding": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "从供应商同步素材组合",
|
||||
"fontSize": "20px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "sync_form",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/sync_from_vendor.dspy')}}",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "vendor",
|
||||
"label": "供应商",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "volcengine",
|
||||
"text": "火山方舟"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "project_name",
|
||||
"label": "项目名",
|
||||
"type": "text",
|
||||
"default": "default"
|
||||
},
|
||||
{
|
||||
"name": "apikey",
|
||||
"label": "Access Key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "secretkey",
|
||||
"label": "Secret Key",
|
||||
"type": "password"
|
||||
}
|
||||
],
|
||||
"submit_label": "开始同步"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
75
wwwroot/upload_asset.ui
Normal file
75
wwwroot/upload_asset.ui
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"padding": "16px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "上传素材",
|
||||
"fontSize": "20px",
|
||||
"fontWeight": "bold",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "upload_form",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/rl_asset_create.dspy')}}",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "group_id",
|
||||
"label": "素材组合",
|
||||
"type": "text",
|
||||
"placeholder": "选择本地组合ID"
|
||||
},
|
||||
{
|
||||
"name": "source_url",
|
||||
"label": "素材URL",
|
||||
"type": "text",
|
||||
"placeholder": "https://... 可公开访问的图片/视频URL"
|
||||
},
|
||||
{
|
||||
"name": "asset_type",
|
||||
"label": "素材类型",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"value": "Image",
|
||||
"text": "图片"
|
||||
},
|
||||
{
|
||||
"value": "Video",
|
||||
"text": "视频"
|
||||
},
|
||||
{
|
||||
"value": "Audio",
|
||||
"text": "音频"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"label": "素材名称",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "apikey",
|
||||
"label": "Access Key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "secretkey",
|
||||
"label": "Secret Key",
|
||||
"type": "password"
|
||||
}
|
||||
],
|
||||
"submit_label": "上传素材"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user