diff --git a/CLAUDE.md b/CLAUDE.md index d716ba8..918689f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,34 @@ jimeng-clone/ | `/admin/assets` | AdminAssetsPage | Admin | Content assets (team→member→video hierarchy) | | `/team/assets` | TeamAssetsPage | TeamAdmin | Team content assets (member→video hierarchy) | +## AI Skills Reference + +本项目用 **skill**(可复用方法论)+ **memory**(个人纪律)+ **hook**(系统强制)三层指引 AI 开发。按编辑场景对应加载: + +| 编辑场景 | 应用的 skill / memory / hook | +|---|---| +| `backend/utils/seedance_client.py`、任何 Seedance API 对接 | **Skill** `seedance-api-integration` — 参数构造、token 计费、异步轮询、错误码映射、asset:// 引用、白标、5 起真实踩坑 | +| Django models 新增字段(`backend/apps/*/models.py`)或改 create() 调用 | **Memory** `feedback_mysql_default` / `feedback_mysql_explicit_fields` — MySQL 严格模式下 create() 必须显式传所有 CharField 值(已 1 起 2026-03-19 线上 500 事故) | +| 任何 `git push` 操作 | **Hook** `~/.claude/hooks/pre-git-push.sh` 会拦截;用户明确授权后用 `ALLOW_PUSH=1 git push` 重试 | +| 用户可见 label / 按钮 / 错误提示 | **Memory** `feedback_write_full_labels`(单位/类型/含义写全)+ `feedback_user_facing_docs_plain_language`(禁程序员术语)+ `feedback_seconds_unit`(只用秒,不换分钟/小时) | +| 任何 user-facing 字符串含模型名 / 错误提示 / 帮助文案 | **Memory** `feedback_no_seedance_branding` — 白标合同禁出现 "Seedance",用 "AirDrama" / "AirDrama Fast" | +| 内联编辑按钮(保存 / 取消) | **Memory** `feedback_inline_edit_style` — `whiteSpace: 'nowrap'` 必加,历史多次被挤成两行 | +| 对接火山 / 豆包 / Seedance 等第三方 API 添加新字段或错误码 | **Memory** `feedback_follow_official_api` — 严格按官方文档,不瞎编字段 / 错误码(历史:曾脑补 `.PolicyViolation` 子类型) | +| 生成页 / `@` mention / 素材库相关测试 | 用 `tudou`(团管)账号登录;`admin` 账号默认没 team,进不了生成页 | +| 改完代码、宣称"完成"之前 | **Memory** `feedback_verify_before_deliver` + `feedback_verify_thoroughly` — 必须自己跑完整 user 路径验证关键改动,不让用户替我们背锅 | +| 商业级代码要求(并发、失败、资源复用) | **Memory** `feedback_commercial_grade` — 不准说"可以接受 / 后续优化",要么做到位要么明确标风险 | +| 改方案 / 改 UI 前发现截图或讨论 | **Memory** `feedback_no_rush_changes` + `feedback_simple_first` — 先提方案(最简单的先给)等用户说"改吧"再动手 | + +**纪律分级:** +- **Skill** — 跨项目可复用的大块方法论(`~/.claude/skills/`) +- **Memory** — 协作纪律 / 个人偏好(自动加载到 session context,在 `~/.claude/projects/c--Airlabs-Project/memory/`) +- **Hook** — 系统级硬拦截(`~/.claude/settings.json` + `~/.claude/hooks/`) +- 项目特定架构约定直接写在本 CLAUDE.md 里(Project Architecture / API Endpoints / Database Models 等章节) + +**增删映射项时:** 新增跨项目规则写 memory、本项目特定规则写 CLAUDE.md 对应章节、跨项目技术方法论才做 skill(参考 `feedback_no_skill_bloat`)。 + +--- + ## Incremental Development Guide ### How to Add Features to This Project diff --git a/backend/apps/generation/views.py b/backend/apps/generation/views.py index 5f184e7..a4b014b 100644 --- a/backend/apps/generation/views.py +++ b/backend/apps/generation/views.py @@ -69,6 +69,27 @@ def _sum_video_duration(references): return total +def _format_prompt_for_ark(prompt, label_placeholders): + """把 prompt 中的 @label 替换为火山可识别的「图片N/视频N/音频N」。 + + 火山 Seedance 模型只能理解"素材类型+序号"格式的指代(官方文档 FAQ Q3); + 文件名 / asset id / URL 对它都是不可理解的字符串,会按位置概率性对齐, + 表现为"人物颠倒"。此函数在发给火山之前做一次静默替换,用户 prompt 原文 + 保留在 DB 便于 reEdit 回填带缩略图的标签。 + + label_placeholders: [(label, placeholder), ...] 调用方需保证按 label 长度 + 降序,防止"碧"先于"碧碧"被替换的子串吞噬问题。 + + 用 str.replace 而非 re.sub,避免 label 含正则元字符(如 "@[test].png")时崩溃。 + """ + result = prompt + for label, placeholder in label_placeholders: + if not label: + continue + result = result.replace(f'@{label}', placeholder) + return result + + def _get_token_price(config, model, has_video_ref, resolution): """根据模型、是否含视频、分辨率选择单价。 @@ -328,6 +349,20 @@ def video_generate_view(request): seen_urls = set() # 去重:同一个素材只引用一次 _asset_cache = {} # group_id → [(asset_url, asset_type), ...],避免同一素材组重复查询 + # 火山规范要求 prompt 里用「图片N/视频N/音频N」指代素材(不能用文件名/asset id)。 + # 循环同步维护各类型 counter 和 label→placeholder 映射,循环结束后一次性替换 prompt。 + # 不变量:任意时刻 image_n / video_n / audio_n == content_items 里该类型 *_url 已 push 的个数。 + label_to_placeholder: dict = {} + image_n = video_n = audio_n = 0 + + def _placeholder_for(asset_type): + """读取当前 counter 值对应的 placeholder。调用前 counter 必须已递增。""" + if asset_type == 'Video': + return f'视频{video_n}' + if asset_type == 'Audio': + return f'音频{audio_n}' + return f'图片{image_n}' + from .models import Asset as AssetModel def _resolve_asset_group_all(gid, lbl): @@ -408,11 +443,16 @@ def video_generate_view(request): aid = 'asset-' + aid[6:] resolved_asset_url = f'asset://{aid}' if asset_obj.asset_type == 'Video': + video_n += 1 content_items.append({'type': 'video_url', 'video_url': {'url': resolved_asset_url}, 'role': 'reference_video'}) elif asset_obj.asset_type == 'Audio': + audio_n += 1 content_items.append({'type': 'audio_url', 'audio_url': {'url': resolved_asset_url}, 'role': 'reference_audio'}) else: + image_n += 1 content_items.append({'type': 'image_url', 'image_url': {'url': resolved_asset_url}, 'role': 'reference_image'}) + if label and label not in label_to_placeholder: + label_to_placeholder[label] = _placeholder_for(asset_obj.asset_type) except AssetModel.DoesNotExist: return Response({ 'error': 'asset_not_found', @@ -442,11 +482,17 @@ def video_generate_view(request): }, status=status.HTTP_400_BAD_REQUEST) for asset_url, asset_type in asset_list: if asset_type == 'Video': + video_n += 1 content_items.append({'type': 'video_url', 'video_url': {'url': asset_url}, 'role': 'reference_video'}) elif asset_type == 'Audio': + audio_n += 1 content_items.append({'type': 'audio_url', 'audio_url': {'url': asset_url}, 'role': 'reference_audio'}) else: + image_n += 1 content_items.append({'type': 'image_url', 'image_url': {'url': asset_url}, 'role': 'reference_image'}) + # 老兼容路径:一个 label 对应 N 张图,展开成"图片N"会改变语义,不登记 label。 + # 但 counter 必须继续递增,否则后续 local 分支的编号会错位。 + logger.warning('legacy asset://group-%s used (label=%s), skip @-replacement (counter advanced by %d)', group_id, label, len(asset_list)) except Exception as e: logger.warning('Failed to resolve asset group URL %s: %s', url, e) return Response({ @@ -456,6 +502,7 @@ def video_generate_view(request): continue # 素材组已展开为多个 content_items,跳过下面的单项处理 if ref_type == 'image': + image_n += 1 item = {'type': 'image_url', 'image_url': {'url': url}} # API 文档要求:参考图模式下所有图片的 role 必须为 reference_image if mode == 'universal': @@ -464,15 +511,25 @@ def video_generate_view(request): item['role'] = role content_items.append(item) elif ref_type == 'video': + video_n += 1 item = {'type': 'video_url', 'video_url': {'url': url}} if role: item['role'] = role content_items.append(item) elif ref_type == 'audio': + audio_n += 1 item = {'type': 'audio_url', 'audio_url': {'url': url}} if role: item['role'] = role content_items.append(item) + else: + # 防御性:未知 ref_type(脏数据或未来扩展)→ 不推 content_item, 不登记 + logger.warning('unknown ref_type=%s url=%s label=%s, skipped', ref_type, url, label) + continue + + if label and label not in label_to_placeholder: + _type_map = {'image': 'Image', 'video': 'Video', 'audio': 'Audio'} + label_to_placeholder[label] = _placeholder_for(_type_map[ref_type]) logger.info('Video generate: %d content_items built (prompt=%s...)', len(content_items), prompt[:60]) @@ -501,9 +558,14 @@ def video_generate_view(request): # ── 调用 AirDrama API(事务外,避免持锁) ── from django.conf import settings as django_settings if django_settings.SEEDANCE_ENABLED and django_settings.ARK_API_KEY: + # 按火山规范把 @label 替换为「图片N/视频N/音频N」;DB 的 record.prompt 仍保留原文 + sorted_pairs = sorted(label_to_placeholder.items(), key=lambda kv: -len(kv[0])) + api_prompt = _format_prompt_for_ark(prompt, sorted_pairs) + logger.info('[ark-prompt] original=%s | converted=%s | mapping=%s', + prompt, api_prompt, label_to_placeholder) try: ark_response = create_task( - prompt=prompt, + prompt=api_prompt, model=model, content_items=content_items, aspect_ratio=aspect_ratio, @@ -1845,6 +1907,8 @@ def _settings_dict(config): 'base_token_price_video': float(config.base_token_price_video), 'base_token_price_fast': float(config.base_token_price_fast), 'base_token_price_fast_video': float(config.base_token_price_fast_video), + 'base_token_price_1080p': float(config.base_token_price_1080p), + 'base_token_price_1080p_video': float(config.base_token_price_1080p_video), 'announcement': config.announcement, 'announcement_enabled': config.announcement_enabled, 'max_desktop_sessions': config.max_desktop_sessions, @@ -3204,15 +3268,20 @@ def asset_group_detail_view(request, group_id): return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND) if request.method == 'DELETE': - # Delete all remote assets in this group from utils import assets_client - for asset in Asset.objects.filter(group=group): - if asset.remote_asset_id: - try: - assets_client.delete_asset(asset.remote_asset_id) - except Exception as e: - logger.warning('Failed to delete remote asset %s: %s', asset.remote_asset_id, e) - # Delete local records + from utils.assets_client import AssetsAPIError + if group.remote_group_id: + try: + assets_client.delete_asset_group(group.remote_group_id) + except AssetsAPIError as e: + # 火山那边已经没了(比如被后台手动删了)就继续清本地,保证幂等 + if e.code != 'NotFound.group_id': + logger.warning('Failed to delete remote group %s: %s', group.remote_group_id, e) + return Response( + {'error': 'assets_api_error', 'message': e.user_message}, + status=status.HTTP_502_BAD_GATEWAY, + ) + logger.info('Remote group %s already gone, cleaning local only', group.remote_group_id) Asset.objects.filter(group=group).delete() group.delete() return Response({'message': '素材组已删除'}) diff --git a/backend/tests/test_1080p_api.py b/backend/tests/test_1080p_api.py index 72e348a..3b8398b 100644 --- a/backend/tests/test_1080p_api.py +++ b/backend/tests/test_1080p_api.py @@ -127,5 +127,38 @@ class TestVideoGenerateResolution(TestCase): self.assertNotEqual(body.get('error'), 'invalid_resolution') +class TestAdminSettingsResponse(TestCase): + """GET /api/v1/admin/settings 必须返回所有 token_price 字段, + 以防 v0.19.0 那种"字段在 serializer 里加了、但 _settings_dict 漏了"的回归。""" + + def setUp(self): + QuotaConfig.objects.get_or_create(pk=1) + self.admin = User.objects.create_user( + username='test_admin_settings', + email='test_admin_settings@example.com', + password='testpass123', + is_staff=True, + is_superuser=True, + ) + self.client = APIClient() + self.client.force_authenticate(user=self.admin) + + def test_get_returns_all_token_price_fields(self): + """GET 返回 4 档单价(全部分辨率 + 是否含视频),缺一不可 — 缺字段会导致前端输入框显示空。""" + resp = self.client.get('/api/v1/admin/settings') + self.assertEqual(resp.status_code, 200) + body = resp.json() + for field in ( + 'base_token_price', + 'base_token_price_video', + 'base_token_price_fast', + 'base_token_price_fast_video', + 'base_token_price_1080p', + 'base_token_price_1080p_video', + ): + self.assertIn(field, body, f'GET /admin/settings response missing {field!r} — 前端这个输入框会显示空') + self.assertIsInstance(body[field], (int, float), f'{field} 应该是数字类型') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/backend/tests/test_ark_prompt_format.py b/backend/tests/test_ark_prompt_format.py new file mode 100644 index 0000000..6922cfe --- /dev/null +++ b/backend/tests/test_ark_prompt_format.py @@ -0,0 +1,239 @@ +""" +测试 prompt 转火山「图片N/视频N/音频N」格式 — v0.19.1+ + +火山模型无法理解文件名/asset id,必须用「素材类型+序号」指代(官方文档 FAQ Q3)。 +本测试文件覆盖: + 单元测试:纯函数 _format_prompt_for_ark + 集成测试:video_generate_view 端到端(含 counter 对齐关键回归) +""" +import os +import sys +import django +from unittest import mock + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +django.setup() + +import unittest +from django.test import TestCase, override_settings +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from apps.accounts.models import Team +from apps.generation.models import QuotaConfig, AssetGroup, Asset, GenerationRecord +from apps.generation.views import _format_prompt_for_ark + +User = get_user_model() + + +# ──────────────────────────────────────────────── +# 单元测试:纯函数 _format_prompt_for_ark +# ──────────────────────────────────────────────── + +class TestFormatPromptForArk(unittest.TestCase): + """覆盖各种 label 替换场景(字符串级别)。""" + + def test_basic_replacement(self): + """@label 替换为 placeholder。""" + out = _format_prompt_for_ark('@碧碧.jpg 是碧儿', [('碧碧.jpg', '图片1')]) + self.assertEqual(out, '图片1 是碧儿') + + def test_multi_type_independent_counters(self): + """图片/视频/音频各自独立编号。""" + pairs = [ + ('img1.jpg', '图片1'), + ('video1.mp4', '视频1'), + ('audio1.mp3', '音频1'), + ] + out = _format_prompt_for_ark('用 @img1.jpg @video1.mp4 和 @audio1.mp3', pairs) + self.assertEqual(out, '用 图片1 视频1 和 音频1') + + def test_same_label_multiple_at_signs(self): + """同一 label 在 prompt 里 @ 多次,全部替换成同一 placeholder(str.replace 全局)。""" + out = _format_prompt_for_ark('@foo 然后 @foo 再 @foo', [('foo', '图片1')]) + self.assertEqual(out, '图片1 然后 图片1 再 图片1') + + def test_substring_conflict_long_first(self): + """当存在子串关系('碧' 是 '碧碧' 的子串),长 label 必须先替换。""" + # 模拟调用方已经按长度降序传入 + pairs = [('碧碧', '图片2'), ('碧', '图片1')] + out = _format_prompt_for_ark('@碧碧 和 @碧 是姐妹', pairs) + self.assertEqual(out, '图片2 和 图片1 是姐妹') + + def test_label_with_regex_metachars(self): + """label 含正则元字符([ ] + . * ? 等),str.replace 不当正则处理。""" + pairs = [ + ('[test].png', '图片1'), + ('a+b.png', '图片2'), + ('a.b*.png', '图片3'), + ] + prompt = '@[test].png 和 @a+b.png 还有 @a.b*.png' + out = _format_prompt_for_ark(prompt, pairs) + self.assertEqual(out, '图片1 和 图片2 还有 图片3') + + def test_empty_mapping(self): + """无 @ 素材的 prompt 原样返回。""" + out = _format_prompt_for_ark('今天天气真好', []) + self.assertEqual(out, '今天天气真好') + + def test_label_in_mapping_not_in_prompt(self): + """mapping 里有 label 但 prompt 里没 @ → 不动。""" + out = _format_prompt_for_ark('一段普通文字', [('foo.jpg', '图片1')]) + self.assertEqual(out, '一段普通文字') + + def test_chinese_punctuation_around_label(self): + """中文标点不影响替换。""" + out = _format_prompt_for_ark('@碧碧.jpg,说:"你好。"', [('碧碧.jpg', '图片1')]) + self.assertEqual(out, '图片1,说:"你好。"') + + def test_empty_label_skipped(self): + """label 为空字符串时跳过,不崩溃。""" + out = _format_prompt_for_ark('@real.jpg 内容', [('', '图片0'), ('real.jpg', '图片1')]) + self.assertEqual(out, '图片1 内容') + + +# ──────────────────────────────────────────────── +# 集成测试:video_generate_view +# ──────────────────────────────────────────────── + +@override_settings(SEEDANCE_ENABLED=True, ARK_API_KEY='fake-test-key') +class TestVideoGenerateArkPrompt(TestCase): + """经 POST /api/v1/video/generate 验证 prompt 转换 + DB 原文保留 + counter 对齐。""" + + def setUp(self): + QuotaConfig.objects.get_or_create(pk=1) + self.team = Team.objects.create( + name='test-ark-prompt', + is_active=True, + monthly_spending_limit=10000, + markup_percentage=0, + balance=10000, + frozen_amount=0, + ) + self.user = User.objects.create_user( + username='ark_prompt_user', + email='arkprompt@example.com', + password='testpass123', + team=self.team, + spending_limit=-1, + daily_generation_limit=-1, + monthly_generation_limit=-1, + ) + self.client = APIClient() + self.client.force_authenticate(user=self.user) + + # 建两个 local asset 方便多场景复用 + self.group_a = AssetGroup.objects.create( + team=self.team, remote_group_id='group-fake-a', name='角色A', + ) + self.asset_bibi = Asset.objects.create( + group=self.group_a, remote_asset_id='asset-fake-bibi', name='碧碧.jpg', + url='https://fake/bibi.jpg', asset_type='Image', status='active', + ) + self.asset_bubu = Asset.objects.create( + group=self.group_a, remote_asset_id='asset-fake-bubu', name='布布.jpg', + url='https://fake/bubu.jpg', asset_type='Image', status='active', + ) + + def _post_generate(self, prompt, references): + return self.client.post('/api/v1/video/generate', { + 'prompt': prompt, + 'mode': 'universal', + 'model': 'seedance_2.0', + 'aspect_ratio': '9:16', + 'duration': 5, + 'resolution': '720p', + 'references': references, + }, format='json') + + @mock.patch('apps.generation.tasks.poll_video_task') + @mock.patch('apps.generation.views.create_task') + def test_view_converts_prompt_for_local_assets(self, mock_create_task, mock_poll): + """prompt 里两个 @local 素材 → 发给火山的 prompt 变成「图片1/图片2」。""" + mock_create_task.return_value = {'id': 'ark-mock-1'} + prompt = '@碧碧.jpg 是碧儿,@布布.jpg 是步若' + resp = self._post_generate(prompt, [ + {'url': f'asset://local-{self.asset_bibi.id}', 'type': 'image', 'label': '碧碧.jpg'}, + {'url': f'asset://local-{self.asset_bubu.id}', 'type': 'image', 'label': '布布.jpg'}, + ]) + self.assertEqual(resp.status_code, 202, resp.content) + self.assertTrue(mock_create_task.called, 'create_task must be called') + sent_prompt = mock_create_task.call_args.kwargs['prompt'] + self.assertEqual(sent_prompt, '图片1 是碧儿,图片2 是步若') + + @mock.patch('apps.generation.tasks.poll_video_task') + @mock.patch('apps.generation.views.create_task') + def test_view_db_prompt_unchanged_for_reedit(self, mock_create_task, mock_poll): + """DB.prompt 必须保留用户原文(含 @xxx.jpg),reEdit 才能重建带缩略图的标签。""" + mock_create_task.return_value = {'id': 'ark-mock-2'} + prompt = '@碧碧.jpg 走过来' + resp = self._post_generate(prompt, [ + {'url': f'asset://local-{self.asset_bibi.id}', 'type': 'image', 'label': '碧碧.jpg'}, + ]) + self.assertEqual(resp.status_code, 202, resp.content) + rec = GenerationRecord.objects.filter(user=self.user).order_by('-id').first() + self.assertIsNotNone(rec) + self.assertEqual(rec.prompt, prompt) # 原文,不含 '图片1' + self.assertIn('@碧碧.jpg', rec.prompt) + self.assertNotIn('图片1', rec.prompt) + + @mock.patch('apps.generation.tasks.poll_video_task') + @mock.patch('apps.generation.views.create_task') + def test_legacy_group_url_skips_replacement(self, mock_create_task, mock_poll): + """asset://group-{id} 老路径:counter 递增但不登记 label,WARNING 日志,@组名 原样留在 prompt。""" + mock_create_task.return_value = {'id': 'ark-mock-3'} + prompt = '@角色A 做动作' + with self.assertLogs('apps.generation.views', level='WARNING') as cm: + resp = self._post_generate(prompt, [ + {'url': f'asset://group-{self.group_a.id}', 'type': 'image', 'label': '角色A'}, + ]) + self.assertEqual(resp.status_code, 202, resp.content) + sent_prompt = mock_create_task.call_args.kwargs['prompt'] + self.assertEqual(sent_prompt, prompt) # 未替换 + # 验证 WARNING log + self.assertTrue(any('legacy asset://group-' in line for line in cm.output), + f'expected legacy warning, got: {cm.output}') + + @mock.patch('apps.generation.tasks.poll_video_task') + @mock.patch('apps.generation.views.create_task') + def test_counter_alignment_with_mixed_local_and_group(self, mock_create_task, mock_poll): + """关键回归:group 展开 2 张图后,紧跟的 local asset 的 label 必须映射到「图片3」不是「图片1」。""" + mock_create_task.return_value = {'id': 'ark-mock-4'} + prompt = '@foo 是主角' + resp = self._post_generate(prompt, [ + {'url': f'asset://group-{self.group_a.id}', 'type': 'image', 'label': '角色A'}, # 展开 2 张图 + {'url': f'asset://local-{self.asset_bibi.id}', 'type': 'image', 'label': 'foo'}, + ]) + self.assertEqual(resp.status_code, 202, resp.content) + sent_prompt = mock_create_task.call_args.kwargs['prompt'] + # foo 对应第 3 张 image(group 两张在前) + self.assertEqual(sent_prompt, '图片3 是主角') + # content_items 验证长度 + sent_content_items = mock_create_task.call_args.kwargs['content_items'] + image_items = [it for it in sent_content_items if it['type'] == 'image_url'] + self.assertEqual(len(image_items), 3) + + @mock.patch('apps.generation.tasks.poll_video_task') + @mock.patch('apps.generation.views.create_task') + def test_counter_alignment_mixed_types(self, mock_create_task, mock_poll): + """图片/音频独立计数 — 图片序号不因音频夹在中间而跳变。""" + mock_create_task.return_value = {'id': 'ark-mock-5'} + # 新建一个 audio asset + asset_audio = Asset.objects.create( + group=self.group_a, remote_asset_id='asset-fake-audio', name='speech.mp3', + url='https://fake/speech.mp3', asset_type='Audio', status='active', + ) + prompt = '@碧碧.jpg 说 @speech.mp3 的话,@布布.jpg 听' + resp = self._post_generate(prompt, [ + {'url': f'asset://local-{self.asset_bibi.id}', 'type': 'image', 'label': '碧碧.jpg'}, + {'url': f'asset://local-{asset_audio.id}', 'type': 'audio', 'label': 'speech.mp3'}, + {'url': f'asset://local-{self.asset_bubu.id}', 'type': 'image', 'label': '布布.jpg'}, + ]) + self.assertEqual(resp.status_code, 202, resp.content) + sent_prompt = mock_create_task.call_args.kwargs['prompt'] + # 碧碧=图片1, speech=音频1, 布布=图片2(图片/音频独立计数) + self.assertEqual(sent_prompt, '图片1 说 音频1 的话,图片2 听') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/backend/utils/assets_client.py b/backend/utils/assets_client.py index d34d589..e7475b7 100644 --- a/backend/utils/assets_client.py +++ b/backend/utils/assets_client.py @@ -83,6 +83,7 @@ def _get_service(): 'UpdateAssetGroup': ApiInfo('POST', '/', {'Action': 'UpdateAssetGroup', 'Version': API_VERSION}, {}, {}), 'UpdateAsset': ApiInfo('POST', '/', {'Action': 'UpdateAsset', 'Version': API_VERSION}, {}, {}), 'DeleteAsset': ApiInfo('POST', '/', {'Action': 'DeleteAsset', 'Version': API_VERSION}, {}, {}), + 'DeleteAssetGroup': ApiInfo('POST', '/', {'Action': 'DeleteAssetGroup', 'Version': API_VERSION}, {}, {}), } return Service(service_info, api_info) @@ -225,3 +226,9 @@ def delete_asset(asset_id: str): """Delete a single asset from the remote API.""" body = {'Id': asset_id, 'ProjectName': PROJECT_NAME} _do_request('DeleteAsset', body) + + +def delete_asset_group(group_id: str): + """Delete an asset group and cascade-delete all its assets on the remote API.""" + body = {'Id': group_id, 'ProjectName': PROJECT_NAME} + _do_request('DeleteAssetGroup', body) diff --git a/docs/API文档/about-Asset-素材组相关/删除素材资产组 (DeleteAssetGroup) b/docs/API文档/about-Asset-素材组相关/删除素材资产组 (DeleteAssetGroup) new file mode 100644 index 0000000..36a7a14 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/删除素材资产组 (DeleteAssetGroup) @@ -0,0 +1,81 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=DeleteAssetGroup&Version=2024-01-01` +删除素材资产组(Asset Group)。 +:::warning + +* 删除素材组将批量删除组内所有素材资产,该操作不可逆,一经删除,不可恢复,请谨慎操作。 +* 如待删除的素材组包含较多素材资产,删除操作可能耗费一定时间。 +* 对于在方舟控制台创建的真人素材组,**仅可删除授权已过期或已拒绝接收的素材组;** 授权有效期内、有效期未开始或已接收的素材无法删除。 + + +::: +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2333601) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +需删除的素材资产组 ID。 + +--- + + +**ProjectName** `string` +需删除素材资产组所属的项目名称,默认值为 default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 +:::tip +本接口无业务返回参数。 + +::: +--- + + + +## 请求示例 +```text +POST /?Action=DeleteAssetGroup&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "group-2026**********-*****", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "DeleteAssetGroup", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": {} +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/删除素材资产(DeleteAsset) b/docs/API文档/about-Asset-素材组相关/删除素材资产(DeleteAsset) new file mode 100644 index 0000000..225cc97 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/删除素材资产(DeleteAsset) @@ -0,0 +1,74 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=DeleteAsset&Version=2024-01-01` +本文介绍删除素材资产(Asset)API 的输入输出参数,供您使用接口时查阅字段含义。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2333601) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +需要删除的 Asset(素材资产)的 Id。 + +--- + + +**ProjectName** `string` +需要删除的 Asset(素材资产)所属的项目名称,默认值为default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 +:::tip +本接口无业务返回参数。 + +::: +--- + + + +## 请求示例 +```text +POST /?Action=DeleteAsset&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "Asset-2026**********-*****", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "DeleteAsset", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": {} +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/更新素材资产信息(UpdateAsset) b/docs/API文档/about-Asset-素材组相关/更新素材资产信息(UpdateAsset) new file mode 100644 index 0000000..c6b91d9 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/更新素材资产信息(UpdateAsset) @@ -0,0 +1,86 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=UpdateAsset&Version=2024-01-01` +本文介绍更新素材资产信息(Asset)API 的输入输出参数,供您使用接口时查阅字段含义。当前仅支持更新 `Name`。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2318269) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +需要更新的 Asset(素材资产)的 Id。 + +--- + + +**Name** `string` +需要更新的 Asset(素材资产)的新名称,上限为 64 个字符。 + +--- + + +**ProjectName** `string` +需要更新的 Asset(素材资产)所属的项目名称,默认值为default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**Id** `string` +Asset(素材资产)的 Id。 + +--- + + + +## 请求示例 +```text +POST /?Action=UpdateAsset&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "Asset-2026**********-*****", + "Name": "new-name", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "UpdateAsset", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "Id": "Asset-2026**********-*****" + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/更新素材资产组合信息(UpdateAssetGroup) b/docs/API文档/about-Asset-素材组相关/更新素材资产组合信息(UpdateAssetGroup) new file mode 100644 index 0000000..5672738 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/更新素材资产组合信息(UpdateAssetGroup) @@ -0,0 +1,93 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=UpdateAssetGroup&Version=2024-01-01` +更新单个 Asset Group(素材资产组合)信息。当前仅支持更新 Asset Group(素材资产组合)的 Name 和 Description。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2318269) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +需要更新的 Asset Group(素材资产组合)的 Id。 + +--- + + +**Name** `string` +需要更新的 Asset Group(素材资产组合)的新名称,上限为 64 个字符。 + +--- + + +**Description** `string` +需要更新的 Asset Group(素材资产组合)的新描述,上限为 300 字符。 + +--- + + +**ProjectName** `string` +需要更新的 Asset Group(素材资产组合)所属的项目名称,默认值为default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**Id** `string` +Asset Group(素材资产组合)的 Id。 + +--- + + + +## 请求示例 +```text +POST /?Action=UpdateAssetGroup&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "group-2026**********-*****", + "Name": "new-name", + "Description": "new-description", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "UpdateAssetGroup", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "Id": "group-2026**********-*****" + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/查询素材资产信息(GetAsset) b/docs/API文档/about-Asset-素材组相关/查询素材资产信息(GetAsset) new file mode 100644 index 0000000..5c290c0 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/查询素材资产信息(GetAsset) @@ -0,0 +1,168 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=GetAsset&Version=2024-01-01` +查询素材资产状态,确认素材是否已完成预处理并可用于推理。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2318269) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +Asset(素材资产)的 Id。 + +--- + + +**ProjectName** `string` +需要查询的 Asset(素材资产)所属的项目名称,默认值为 `default`。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**Id** `string` +Asset(素材资产)的 Id。 + +--- + + +**Name** `string` +Asset(素材资产)的名称,上限为 64 个字符。 + +--- + + +**URL** `string` +Asset(素材资产)的访问地址。有效期为 12 小时,请及时保存。 + +--- + + +**AssetType** `string` +Asset(素材资产)的类型。可选值: + +* `Image` +* `Video` +* `Audio` + + +--- + + +**GroupId** `string` +Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +--- + + +**Status** `string` +素材资产状态。可选值: + +* `Active`:已处理完毕,可以使用 +* `Processing`:正在预处理,无法使用 +* `Failed`:处理失败 + + +--- + + +**Error** `object` +错误信息。 +属性 + +--- + + + Error.**Code** `string` + 错误码。 + +--- + + + Error.**Message** `string` + 错误信息。 + +--- + + +**CreateTime** `string` +创建时间。 + +--- + + +**UpdateTime** `string` +更新时间。 + +--- + + +**ProjectName** `string` +资源所属的项目名称。 + +--- + + + +## 请求示例 +```text +POST /?Action=GetAsset&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "Asset-2026**********-*****", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "GetAsset", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "Id": "Asset-2026**********-*****", + "Name": "test", + "URL": "https://example.com/asset-url", + "AssetType": "Image", + "GroupId": "group-2026**********-*****", + "Status": "Active", + "Error": { + "Code": "", + "Message": "" + }, + "CreateTime": "2026-03-28T00:00:00Z", + "UpdateTime": "2026-03-28T00:00:00Z", + "ProjectName": "default" + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/查询素材资产列表(ListAssets) b/docs/API文档/about-Asset-素材组相关/查询素材资产列表(ListAssets) new file mode 100644 index 0000000..05d3697 --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/查询素材资产列表(ListAssets) @@ -0,0 +1,280 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=ListAssets&Version=2024-01-01` +查询符合筛选条件的 Assets(素材资产)列表。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2318269) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Filter** `object` %%require%% +搜索的过滤条件。 +属性 + +--- + + + Filter.**GroupIds** `string[]` + Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id 列表。 + +--- + + + Filter.**GroupType** `string` %%require%% + Asset Group(素材资产组合)的类型。可选值: + + * `AIGC`:虚拟人像 + * `LivenessFace`:真人素材 + + +--- + + + Filter.**Statuses** `string[]` + 素材资产状态。可选值: + + * `Active`:素材资产已处理完毕,可以使用 + * `Processing`:素材资产正在预处理,无法使用 + * `Failed`:素材资产处理失败 + + +--- + + + Filter.**Name** `string` + Asset(素材资产)的名称,上限为 64 个字符。 + +--- + + +**PageNumber** `integer (i64)` %%require%% +搜索页码,从 1 开始。例如:`1` 表示返回第一页的搜索结果。 + +--- + + +**PageSize** `integer (i64)` %%require%% +每页搜索结果的数量,上限为 100。 + +--- + + +**SortBy** `string` +用于排序的字段名称,默认值为 `CreateTime`。可选值: + +* `CreateTime`:根据创建时间排序 +* `UpdateTime`:根据更新时间排序 +* `GroupId`:根据资产素材组的 Id 排序 + + +--- + + +**SortOrder** `string` +排序顺序,默认值为 `Desc`。可选值: + +* `Desc`:降序 +* `Asc`:升序 + + +--- + + +**ProjectName** `string` +资源所属的项目名称,默认值为default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**Items** `object[]` +符合筛选条件的 Asset(素材资产)数组。 +属性 + +--- + + + Items.**Id** `string` + Asset(素材资产)的 Id。 + +--- + + + Items.**Name** `string` + Asset(素材资产)的名称,上限为 64 个字符。 + +--- + + + Items.**URL** `string` + Asset(素材资产)的公共可访问地址。有效期为 12 小时,请及时保存。 + +--- + + + Items.**GroupId** `string` + Asset(素材资产)所属的 Asset Group(素材资产组合)的 Id。 + +--- + + + Items.**AssetType** `string` + Asset(素材资产)的类型。可选值: + + * `Image`:图像 + * `Video`:视频 + * `Audio`:音频 + + +--- + + + Items.**Status** `string` + 任务状态。可选值: + + * `Active` + * `Processing` + * `Failed` + + +--- + + + Items.**Error** `object` + 错误信息。 + 属性 + +--- + + + Items.Error.**Code** `string` + 错误码。 + +--- + + + Items.Error.**Message** `string` + 错误信息。 + +--- + + + Items.**ProjectName** `string` + 资源所属的项目名称。 + +--- + + + Items.**CreateTime** `string` + 创建时间。 + +--- + + + Items.**UpdateTime** `string` + 更新时间。 + +--- + + +**TotalCount** `integer (i64)` +返回总数。 + +--- + + +**PageNumber** `integer (i64)` +返回的页数。 + +--- + + +**PageSize** `integer (i64)` +每页搜索结果的数量,上限为 100。 + +--- + + + +## 请求示例 +```text +POST /?Action=ListAssets&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Filter": { + "GroupIds": [ + "group-2026**********-*****" + ], + "GroupType": "AIGC", + "Statuses": [ + "Active" + ], + "Name": "test" + }, + "PageNumber": 1, + "PageSize": 10, + "SortBy": "CreateTime", + "SortOrder": "Desc", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "ListAssets", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "Items": [ + { + "Id": "Asset-2026**********-*****", + "Name": "test", + "URL": "https://example.com/asset-url", + "GroupId": "group-2026**********-*****", + "AssetType": "Image", + "Status": "Active", + "Error": { + "Code": "", + "Message": "" + }, + "ProjectName": "default", + "CreateTime": "2026-03-28T00:00:00Z", + "UpdateTime": "2026-03-28T00:00:00Z" + } + ], + "TotalCount": 1, + "PageNumber": 1, + "PageSize": 10 + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/查询素材资产组合信息(GetAssetGroup) b/docs/API文档/about-Asset-素材组相关/查询素材资产组合信息(GetAssetGroup) new file mode 100644 index 0000000..7d9adbb --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/查询素材资产组合信息(GetAssetGroup) @@ -0,0 +1,125 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=GetAssetGroup&Version=2024-01-01` +获取单个Asset Group(素材资产组合)信息。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2333601) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Id** `string` %%require%% +Asset Group(素材资产组合)的 Id。 + +--- + + +**ProjectName** `string` +需要查询的 Asset Group(素材资产组合)所属的项目名称,默认值为default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**Id** `string` +Asset Group(素材资产组合)的 Id。 + +--- + + +**Name** `string` +Asset Group(素材资产组合)的名称,上限为 64 个字符。 + +--- + + +**Description** `string` +Asset Group(素材资产组合)的描述,上限为 300 字符。 + +--- + + +**GroupType** `string` +Asset Group(素材资产组合)的类型。可选值: + +* `AIGC`:虚拟人像 +* `LivenessFace`:真人素材 + + +--- + + +**ProjectName** `string` +资源所属的项目名称。 + +--- + + +**CreateTime** `string` +创建时间。 + +--- + + +**UpdateTime** `string` +更新时间。 + +--- + + + +## 请求示例 +```text +POST /?Action=GetAssetGroup&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Id": "group-2026**********-*****", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "GetAssetGroup", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "Id": "group-2026**********-*****", + "Name": "test", + "Description": "test", + "GroupType": "AIGC", + "ProjectName": "default", + "CreateTime": "2026-03-28T00:00:00Z", + "UpdateTime": "2026-03-28T00:00:00Z" + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/查询素材资产组合列表(ListAssetGroups) b/docs/API文档/about-Asset-素材组相关/查询素材资产组合列表(ListAssetGroups) new file mode 100644 index 0000000..b03ff1f --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/查询素材资产组合列表(ListAssetGroups) @@ -0,0 +1,220 @@ +`POST https://ark.cn-beijing.volcengineapi.com/?Action=ListAssetGroups&Version=2024-01-01` +查询符合筛选条件的Asset Groups(素材资产组合)列表。 + +```mixin-react +return ( +![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_57d0bca8e0d122ab1191b40101b5df75.png =20x) [调用教程](https://www.volcengine.com/docs/82379/2333565) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_f45b5cd5863d1eed3bc3c81b9af54407.png =20x) [接口列表](https://www.volcengine.com/docs/82379/2318269) ![图片](https://portal.volccdn.com/obj/volcfe/cloud-universal-doc/upload_bef4bc3de3535ee19d0c5d6c37b0ffdd.png =20x) [开通模型](https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false) +`}> +); +``` + + +--- + + + +## 请求参数 + +### 请求体 + +--- + + +**Filter** `object` %%require%% +搜索的过滤条件。 + +属性 +Filter.**GroupIds** `array` +Asset Group(素材资产组合)的 Id 列表。 + +--- + + +Filter.**GroupType** `string` %%require%% +Asset Group(素材资产组合)的类型。可选值: + +* `AIGC`:虚拟人像 +* `LivenessFace`: 真人素材 + + +--- + + +Filter.**Name** `string` +Asset Group(素材资产组合)的名称,上限为 64 个字符。 + + + +--- + + +**PageNumber** `integer (i64)` %%require%% +搜索页码,可用于列表分页功能,从 1 开始。例如:"page_number": 1,即返回第一页的搜索结果。 + +--- + + +**PageSize** `integer (i64)` %%require%% +每页搜索结果的数量,上限为 100。 + +--- + + +**SortBy** `string` +用于排序的字段名称,默认值为 `CreateTime`。可选值: + +* `CreateTime`:根据创建时间排序 +* `UpdateTime`:根据更新时间排序 + + +--- + + +**SortOrder** `string` +排序顺序,默认值为 `Desc`。可选值: + +* `Desc`:降序 +* `Asc`:升序 + + +--- + + +**ProjectName** `string` +资源所属的项目名称,默认值为 default。 +若资源不在默认项目中,需填写正确的项目名称,获取项目名称,请查看 [文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +## 响应参数 + +--- + + +**TotalCount ** `int (i64)` +返回的 Asset Group(素材资产组合)的总数。 + +--- + + +**Items** `array[] ` +符合筛选条件的 Asset Group(素材资产组合)数组。 + +--- + + + Items.**Id** `string` + Asset Group(素材资产组合)的 Id。 + +--- + + + Items.**Name** `string` + Asset Group(素材资产组合)的名称,上限为64个字符。 + +--- + + + Items.**Description** `string` + Asset Group(素材资产组合)的描述,上限为 300 字符。 + +--- + + + Items.**GroupType** `string` + Asset Group(素材资产组合)的类型。 + + * `AIGC`:虚拟人像。 + * `LivenessFace`: 真人素材 + + +--- + + + Items.**ProjectName** `string` + 资源所属的项目名称。 + +--- + + + Items.**CreateTime** `string` + 创建时间。 + +--- + + + Items.**UpdateTime** `string` + 更新时间。 + +--- + + + +### **PageNumber ** int (i64) +返回的页数。 + +--- + + + +### **PageSize ** int (i64) +每页搜索结果的数量,上限为100。 + +--- + + + +## 请求示例 +```text +POST /?Action=ListAssetGroups&Version=2024-01-01 HTTP/1.1 +Host: ark.cn-beijing.volcengineapi.com +Content-Type: application/json +X-Date: 20260328T000000Z +X-Content-Sha256: 287e874e******d653b44d21e +Authorization: HMAC-SHA256 Credential=AKLTYz******/20260328/cn-beijing/ark/request, SignedHeaders=content-type;host;x-content-sha256;x-date, Signature=47a7d934******e41085f + +{ + "Filter": { + "Name": "test", + "GroupType": "AIGC" + }, + "PageNumber": 1, + "PageSize": 10, + "SortBy": "CreateTime", + "SortOrder": "Desc", + "ProjectName": "default" +} +``` + + +## 响应示例 +```json +{ + "ResponseMetadata": { + "RequestId": "20260328000000000000000000000000", + "Action": "ListAssetGroups", + "Version": "2024-01-01", + "Service": "ark", + "Region": "cn-beijing" + }, + "Result": { + "TotalCount": 1, + "Items": [ + { + "Id": "group-2026**********-*****", + "Name": "test", + "Title": "test", + "Description": "test", + "GroupType": "AIGC", + "ProjectName": "default", + "CreateTime": "2026-03-28T00:00:00Z", + "UpdateTime": "2026-03-28T00:00:00Z" + } + ], + "PageNumber": 1, + "PageSize": 10 + } +} +``` + + + diff --git a/docs/API文档/about-Asset-素材组相关/私域虚拟人像素材资产库使用指南(邀测用户版).md b/docs/API文档/about-Asset-素材组相关/私域虚拟人像素材资产库使用指南(邀测用户版).md new file mode 100644 index 0000000..b4bdd3f --- /dev/null +++ b/docs/API文档/about-Asset-素材组相关/私域虚拟人像素材资产库使用指南(邀测用户版).md @@ -0,0 +1,820 @@ +:::danger +* 仅限邀测用户阅读,请勿截图/分享给其他人员。 +* 上传素材 (CreateAsset) API 为异步接口,系统处理可能出现排队,导致入库时间增加。不承诺上传时间 SLA。 +* 素材资产应为虚拟人像,非虚拟人像类素材无需入库。 +* 您需确保上传的虚拟人像符合以下条件: + * 您合法拥有该素材,并享有完整的使用及处分权限。素材不包含未获授权的第三方商标、标识类内容。 + * 素材不得与任何自然人肖像或形象雷同,素材不存在抄袭、盗用情形,不会侵害任何第三方的人格权、知识产权等合法权益。 + * 素材不包含违反法规、违背公序良俗、危害国家安全的内容。 +::: +Seedance 2.0 系列模型具有完备的防范 Deepfake 和侵犯版权风险能力。在生成视频时,会对有风险的参考素材输入进行拦截,最大限度保证生成视频合规和安全性。 +为确保创作者能充分利用 Seedance 2.0 强大的视频生成能力高效生成视频内容,同时规避 AI 生成内容的潜在风险,方舟推出了私域可信素材库。完成入库的可信素材将进入您的私域素材库,在视频生成中使用。 +私域素材库使用流程如下: +
+ + +# 素材资产库结构说明 + +* **Asset Group(素材资产组合)**:单个素材文件为一个 Asset,每个 Asset 属于一个 Asset Group。 + * 可以使用素材组自由管理素材,例如可将同一虚拟人物素材放入同一素材组合进行管理。 +* **Asset(素材资产)**:一个素材文件(当前支持上传图像、视频、音频),是方舟 Seedance 2.0 系列模型可直接用于推理的可信资产。 + +:::tip +注意 + +* 仅需入库推理需使用的素材资产,不需使用的素材资产请勿入库。 +* 仅可使用已入库素材资产的 Id (Asset ID) 进行视频生成,同一形象未入库素材无法使用。 +* 每个上传的素材资产需经过预处理,可轮询调用 **GetAsset** 接口查询素材状态(对应参数为 **Status)**,仅当状态变为 `Active` 后,该素材资产方可用于后续推理使用;若状态为 `Failed` 则表示处理失败,无法用于后续推理使用。**详情可参考**[示例:上传素材并使用 GetAsset 获取素材信息](/docs/82379/2333565#5c0ee427)**。** +::: +**以图像资产上传为例:** + +* **单张图片文件格式要求:** + * 格式:jpeg、png、webp、bmp、tiff、gif、heic/heif + * 宽高比(宽/高): (0.4, 2.5) + * 宽高长度(px):(300, 6000) + * 大小:单张图片小于 30 MB。 +* 为保证上传的图片素材资产在后续生成视频时,**人物面部、服装细节等与上传的素材资产一致,​**推荐按照如下规则及示例将同一人物的多个素材传入同一资产组合: + * **人像资产内容最佳实践:** + +:::tip +**全身参考图要求** + +* 板式:竖版 +* 图片内容:人物全身正面图片 +::: +
+ +:::tip +**人脸特写图要求** + +* 板式: 竖版 +* 图片内容:人物正面无表情特写,肩部以上,人物面部占画面2/3左右 +::: +
+ + + +# 素材资产(Assets)API 接口功能 +:::warning +调用素材资产(Assets)API 接口需使用 Access Key 鉴权,详情参考 [获取 API 访问密钥(AK/SK)](https://www.volcengine.com/docs/6257/64983?lang=zh)。 +::: + +## 接口列表 + +## **Asset (Group) 创建接口** + +1. [CreateAssetGroup](https://www.volcengine.com/docs/82379/2318270):创建素材资产组合。**首次创建素材资产组合时需在控制台签署授权函,详情参考**[私域虚拟人像素材资产库使用指南(邀测用户版)](/docs/82379/2333565) +2. [CreateAsset](https://www.volcengine.com/docs/82379/2318271):创建素材资产。该接口可用于上传个人素材资产,创建素材资产后可利用返回字段中的素材 **Id (需处于** **`Active` 状态)**用于 Seedance 2.0 系列模型生成视频。 + + +## **Asset (Group) 管理接口** + +* [ListAssetGroups](https://www.volcengine.com/docs/82379/2318272):查询素材资产组合列表。 +* [ListAssets](https://www.volcengine.com/docs/82379/2318273):查询素材资产列表。 +* [GetAsset](https://www.volcengine.com/docs/82379/2318274):查询素材资产信息。 +* [GetAssetGroup](https://www.volcengine.com/docs/82379/2318275):查询素材资产组合信息。 +* [UpdateAssetGroup](https://www.volcengine.com/docs/82379/2318276):更新素材资产组合信息。 +* [UpdateAsset](https://www.volcengine.com/docs/82379/2318277):更新素材资产信息。 +* [DeleteAsset](https://www.volcengine.com/docs/82379/2318278):删除单个素材资产。 +* [DeleteAssetGroup](https://www.volcengine.com/docs/82379/2341606): 删除指定素材组。 + + +## 限流要求 +:::tip +* **QPS**:API 接口**每秒**允许的请求总数上限,超出则请求报错。 +* **QPM**:API 接口**每分钟**允许的请求总数上限,超出则请求报错。 +::: + +| | | \ +|接口名 |账号维度的限流 | +|---|---| +| | | \ +|CreateAssetGroup |10 QPS | +| | | \ +|CreateAsset |300 QPM | +| | | \ +|ListAssetGroups |10 QPS | +| | | \ +|ListAssets |10 QPS | +| | | \ +|GetAsset |100 QPS | +| | | \ +|GetAssetGroup |10 QPS | +| | | \ +|UpdateAsset |10 QPS | +| | | \ +|UpdateAssetGroup |10 QPS | +| | | \ +|DeleteAsset |10 QPS | +| | | \ +|DeleteAssetGroup |5 QPS | + + +# 使用教程 + +## 上传素材至私域虚拟人像库 (API & 控制台) +您可将自有的虚拟形象上传至私域虚拟人像库。 +:::danger +您需确保上传的虚拟人像符合以下条件: +您合法拥有该素材,并享有完整的使用及处分权限。素材不包含未获授权的第三方商标、标识类内容。 +素材不得与任何自然人肖像或形象雷同,素材不存在抄袭、盗用情形,不会侵害任何第三方的人格权、知识产权等合法权益。 +素材不包含违反法规、违背公序良俗、危害国家安全的内容。 +::: +方舟将对您上传的素材进行安全审核。审核通过后,即可在体验中心和 API 中使用素材生成视频。 +您可使用 OpenAPI 或在体验中心上传虚拟素材。 + +### 阅读并同意协议 +首次入库前,需打开 [控制台](https://console.volcengine.com/ark/region:ark+cn-beijing/overview?briefPage=0&briefType=introduce&type=new) > **开通管理** > **开通素材资产库权限,​**阅读和同意相关规则和协议: +
+ +先创建 Asset Group, 再向 Group 中添加虚拟人像素材。 +:::tip +素材格式的具体要求,请参考[素材资产库结构说明](/docs/82379/2333565#2b7bf522)。 +::: + +### 使用控制台 + +1. 打开 [方舟控制台](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128&tab=GenVideo) > **我的** > **虚拟人像**。 + +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/b1152a6834bc4a7e8f5137474bf34391~tplv-goo7wpa0wc-image.image =1541x) + +2. 创建素材组合。 +3. 向素材组合中上传素材。 + + +### 使用 API +先调用 `CreateAssetGroup` 接口创建素材组合,再调用 `CreateAsset` 接口向组合中上传素材。请求示例: + +1. **创建素材组合** + +:::tip +**注意**: + +* 调用素材资产(Assets)API 接口需使用 Access Key 鉴权,详情参考 [API访问密钥管理](https://www.volcengine.com/docs/6257/64983?lang=zh)。 +* API 参数信息请参考[私域虚拟人像库 API 参考文档](/docs/82379/2333601)。 +* **素材库**[项目](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)**(Project)隔离说明** + 向指定的 Asset Group(素材资产组合)内创建或查询 Asset(素材资产)时,需保证两者的 **ProjectName** 一致 + Asset(素材资产)所属的 **ProjectName** 需与调用视频生成 API 接口时使用的 API key 所属的 **ProjectName** 一致 +::: +使用 **POST` `**`CreateAssetGroup` 接口创建素材组合。 +在请求中传入: + +* **Name**:素材组合的名称。 +* **Description**: 素材组合的文字描述。 +* **GroupType**: 选填,默认为 AIGC(虚拟人像素材)。 + +:::tip +当前仅支持 AIGC 类型。 +::: + +* **ProjectName**:选填,指定资源项目名称,默认为 default。一个项目中的资源仅可被该项目下的推理接入点使用,获取项目名称请参考[文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +:::tip +**注意**: +如果请求中不指定 **ProjectName**,默认将创建素材组至 **default** 项目中。 +::: +请求示例: +**注意**:需使用 AK/SK 鉴权,详情参考 [API访问密钥管理](https://www.volcengine.com/docs/6257/64983?lang=zh)。 +```Go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "CreateAssetGroup", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "Name": "figure_group_1", + "Description": "Figure group 1", + "ProjectName": "", + }, + ) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例: +```JSON +{ + "Id":"group-20260318033332-*****"} +``` + + +2. **上传素材** + +:::danger +上传素材 (CreateAsset) API 为异步接口,系统处理可能出现排队,导致入库时间增加。不承诺上传时间 SLA。 +视频素材处理将耗费更长时间。 +::: +使用 **POST** `CreateAsset`接口上传素材。 +在请求中提供: + +* **GroupId**:必填,素材组合 ID +* **URL**: 必填,图片/视频/音频可访问的 URL +* **AssetType**: 必填,支持上传图片/视频/音频类型素材,需指定为 **Image/Video/Audio。​**素材文件的具体限制详见 [Assets API 参考文档](https://www.volcengine.com/docs/82379/2318271)**。** +* **Name**: 选填,素材名称,可用于管理素材,如素材文件名。 + +:::tip +该字段仅用于使用 ListAssets 接口时模糊搜索素材,不会被带入模型推理。关于如何使用素材生成视频,请参考[使用虚拟人像](/docs/82379/2291680#2bf01416) 和[3. 提示词(content.text)中应该如何准确指代参考素材?](/docs/82379/2333565#15e21eb8)。 +::: + +* **ProjectName**:选填,指定资源项目名称,默认为 **default**。一个项目中的资源仅可被该项目下的推理接入点使用,获取项目名称请参考[文档](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)。 + +:::tip +**注意** +如果请求中不指定 **ProjectName**,则默认上传素材至 **default** 项目中。您需使用该字段确保将素材上传至对应的项目中。 +::: +**注意**: + +* 每次请求上传一个素材文件。 +* 该请求返回素材 ID,可使用 GetAsset API 查看是否上传成功。 + +```Go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "CreateAsset", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "GroupId": "group-20260318070359-*****", + "URL": "", + "AssetType": "Image", + "ProjectName": "" + }, + ) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例: +```JSON +{ + "Id": "asset-20260318071009-*****" +} +``` + + +## 检索虚拟人像资产 (API & 控制台) +您可使用以下方式检索虚拟人像资产。 + +* **控制台**:您可在 [方舟控制台](https://console.volcengine.com/ark/region:ark+cn-beijing/experience/vision?modelId=doubao-seedance-2-0-260128&tab=GenVideo) > **我的** > **我的虚拟人像** 中搜索和查看已上传的虚拟人像资产。 +* **API**: + * **POST** `GetAsset `获取单个素材 + * **POST** `ListAssets` 查询素材 + * **POST** `ListAssetGroups` 查询素材组合信息 + + +### 获取单个素材信息 +可使用 **POST** GetAsset 获取单个素材信息,指定素材资产 ID。 +:::tip +要获取完整的 API 参数、限流等信息,请查看[私域虚拟人像库 API 参考文档](/docs/82379/2333601)。 +::: +```Go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("your_ak", "your_sk", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "GetAsset", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "Id": "asset-20260318070533-*****", + "ProjectName": "", // 需确保填入素材所在项目的名称 + }, + ) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例: +```JSON +{ + "GroupId": "group-20260318033332-*****", + "Status": "Active", + "CreateTime": "2026-03-18T03:57:10Z", + "AssetType": "Image", + "UpdateTime": "2026-03-18T03:57:14Z", + "ProjectName": "default", + "Id": "asset-20260318035710-*****", + "Name": "", + "URL": "https://ark-media-asset-stg.tos-cn-beijing.volces.com/2100000825/031807095608757847.jpg?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=****&X-Tos-Expires=43200&X-Tos-Security-Token=****&X-Tos-Signature=****&X-Tos-SignedHeaders=host" // 有效期为 12 小时 + } +``` + + + +### 查询素材资产 +可使用 **POST** ListAssets 查询 Assets。 + +* 支持根据组合 ID (GroupId)、素材状态(Statuses)和素材名称(Name)查询。筛选出符合所有条件的素材。 +* 支持使用 Name 进行模糊搜索,同时使用 GroupId 精确搜索,便于检索所需的素材。 + +支持使用 SortBy,SortOrder 对结果进行排序。 +```Go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "ListAssets", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "Filter": map[string]any{ + "GroupIds": []string{"group-20260318033332-*****"}, + "GroupType": "AIGC", + "Statuses": []string{"Active", "Processing"}, // 支持 Active(素材上传成功,可使用Asset ID), Processing(素材处理中), Failed(素材上传失败) + "Name": "figure", // 支持模糊搜索 + }, + "PageNumber": 1, + "PageSize": 10, + "SortBy": "GroupId", + "SortOrder": "Asc", + }, + ) + if err != nil { + fmt.Printf("list assets error: %v\n", err) + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例: +```JSON + "Items": [ + { + "Id": "asset-20260318035710-kctzf", + "Name": "", + "AssetType": "Image", + "CreateTime": "2026-03-18T03:57:10Z", + "UpdateTime": "2026-03-18T03:57:14Z", + "ProjectName": "default", + "URL": "image_url", // 有效期为 12 小时 + "GroupId": "group-20260318033332-*****", + "Status": "Active" + }, + { + "GroupId": "group-20260318033332-*****", + "Status": "Active", + "Id": "asset-20260318034804-*****", + "Name": "", + "URL": "https://ark-media-asset-stg.tos-cn-beijing.volces.com/2100000825/031807095608757847.jpg?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=****&X-Tos-Expires=43200&X-Tos-Security-Token=****&X-Tos-Signature=****&X-Tos-SignedHeaders=host", + "AssetType": "Image", + "CreateTime": "2026-03-18T03:48:04Z", + "UpdateTime": "2026-03-18T03:48:08Z", + "ProjectName": "default" + } + ], + "TotalCount": 2, + "PageNumber": 1, + "PageSize": 10 +``` + + +### 查询素材组 +使用 **POST** ListAssetGroups 查询素材组合信息。 +支持模糊搜索素材组合名称(Name)或提供多个素材组合(GroupId)。 +如有多个素材组,可使用 Name 字段进行模糊搜索。 +:::tip +要获取完整的 API 参考文档,请查看[私域虚拟人像库 API 参考文档](/docs/82379/2333601)。 +::: +```Go +package main + +import ( + "fmt" + + "github.com/bytedance/sonic" + "github.com/volcengine/volcengine-go-sdk/volcengine" + "github.com/volcengine/volcengine-go-sdk/volcengine/credentials" + "github.com/volcengine/volcengine-go-sdk/volcengine/session" + "github.com/volcengine/volcengine-go-sdk/volcengine/universal" +) + +func main() { + config := volcengine.NewConfig().WithCredentials(credentials.NewStaticCredentials("", "", "")).WithRegion("cn-beijing") + sess, _ := session.NewSession(config) + resp, err := universal.New(sess).DoCall( + universal.RequestUniversal{ + ServiceName: "ark", + Action: "ListAssetGroups", + Version: "2024-01-01", + HttpMethod: universal.POST, + ContentType: universal.ApplicationJSON, + }, + &map[string]any{ + "Filter": map[string]any{ + "Name": "figure_group", // Support fuzzy search + "GroupIds": []string{"group-20260318033332-*****"}, + "GroupType": "AIGC", + }, + "PageNumber": 1, + "PageSize": 10, + }, + ) + if err != nil { + fmt.Printf("error: %v\n", err) + return + } + if resp == nil { + return + } + respData, err := sonic.Marshal(resp) + fmt.Println(string(respData)) +} +``` + +返回示例: +```JSON +{ + "TotalCount": 1, + "Items": [ + { + "UpdateTime": "2026-03-18T03:33:32Z", + "Id": "group-20260318033332-*****", + "Name": "figure_group_1", + "Title": "figure_group_1", + "Description": "Figure group 1", + "GroupType": "AIGC", + "ProjectName": "default", + "CreateTime": "2026-03-18T03:33:32Z" + } + ], + "PageNumber": 1, + "PageSize": 10 +} +``` + + +### 更新/删除素材和素材组 +请参考: [私域虚拟人像库 API 参考文档](/docs/82379/2333601)。 + +## 示例:上传素材并使用 GetAsset 获取素材信息 +以下示例创建素材资产后,查询资产 Status 并根据状态,判断是否继续查询或返回对应结果。 +代码执行以下逻辑: + +1. createAsset: 上传资源,获取 AssetId +2. waitForAssetActive:开始查询,循环调用 getAssetStatus 查询当前资产状态 +3. 根据 Status 判断 + * Processing → 继续轮询 + * Active → 返回 URL(结束)状态为 `Active` 后,可使用该素材 Asset ID (URI格式) 进行视频生成,如何使用人像素材生成视频,详见[使用虚拟人像](/docs/82379/2291680#2bf01416)。 + * Failed → 返回错误(结束) +4. 返回结果并打印结果 + + +查询结果示意如下: +```JSON +asset status: Active +asset is active, URL = https://ark-media-asset-stg.tos-cn-beijing.volces.com/2100000825/031807095608757847.jpg?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=****&X-Tos-Expires=43200&X-Tos-Security-Token=****&X-Tos-Signature=****&X-Tos-SignedHeaders=host +``` + + +## 其他编程语言示例 +查看更多语言的示例代码请下载: + +:::tip +注意替换 Demo 中的 AK与SK,若需调用其他接口如 ListAssets,需替换 ACTION 与对应请求参数。 +::: + +## 使用人像素材生成视频 +在获取素材 Asset ID后,可使用私域人像素材生成视频。效果预览及使用方式请参考下文。 + +### 视频生成 +在 Video Generation API 的 **content.<模态>_url.url** 字段中使用 素材 URI 生成视频。 +:::tip +资产 URI 拼接方式:`asset://`** +::: +具体方式请参考 [Seedance 2.0 教程](https://www.volcengine.com/docs/82379/2291680?lang=zh) 和 [Seedance 2.0 API 参考](https://www.volcengine.com/docs/82379/1520757?lang=zh)。 +:::tip + +在传入给模型的 Prompt 中,需要使用**图片 1**、**视频 1** 的方式指代参考素材,素材序号为素材在请求体中的顺序。请勿直接在 Prompt 中直接使用 Asset ID。 +例:“**图片1** 里的女孩身着**图片2**中的服装,正在整理柜台上的物品。**图片3**中的男孩是一位顾客,他走上前,想要向女孩索要联系方式。” +调用示例请参考[3. 提示词(content.text)中应该如何准确指代参考素材?](/docs/82379/2333565#15e21eb8) +::: +示例代码: +```Python +import os +import time +# Install SDK: pip install 'volcengine-python-sdk[ark]' +from volcenginesdkarkruntime import Ark +client = Ark( + # The base URL for model invocation + base_url='https://ark.cn-beijing.volces.com/api/v3', + # Get API Key:https://console.volcengine.com/ark/region:ark+cn-beijing/apikey + api_key=os.environ.get("ARK_API_KEY"), +) +if __name__ == "__main__": + print("----- create request -----") + create_result = client.content_generation.tasks.create( + model="doubao-seedance-2-0-260128", # Replace with Model ID + content=[ + { + "type": "text", + "text": "图片1中美妆博主用中文进行介绍,妆容改为明艳大气,去掉脸部反光,笑容甜美,近景镜头,手持图片2的面霜面向镜头展示,清新简约背景,元气甜美风格。博主台词:挖到本命面霜了!质地像云朵一样软糯,一抹就吸收,熬夜急救、补水保湿全搞定,素颜都自带柔光感。" + }, + { + "type": "image_url", + "image_url": { + "url": "asset://asset-20260224200602-qn7wr" # Asset ID + }, + "role": "reference_image" + }, + { + "type": "image_url", + "image_url": { + "url": "https://ark-project.tos-cn-beijing.volces.com/doc_image/r2v_edit_pic1.jpg" + }, + "role": "reference_image" + }, + ], + generate_audio=True, + ratio="16:9", + duration=11, + watermark=True, + ) + print(create_result) + print("----- polling task status -----") + task_id = create_result.id + while True: + get_result = client.content_generation.tasks.get(task_id=task_id) + status = get_result.status + if status == "succeeded": + print("----- task succeeded -----") + print(get_result) + break + elif status == "failed": + print("----- task failed -----") + print(f"Error: {get_result.error}") + break + else: + print(f"Current status: {status}, Retrying after 30 seconds...") + time.sleep(30) +``` + + +# 常见问题 + +#### 1. 为什么素材上传成功后,无法使用素材生成视频或获取素材信息? +素材库按[项目](https://www.volcengine.com/docs/82379/1359411?lang=zh#03ec4a65)**(Project)隔离**。 + +* 在视频生成时,必须使用**素材所在项目**中的推理接入点进行推理。 +* 如果素材上传成功,但使用获取素材接口获取素材失败,可能是因为调用上传素材(CreateAsset)和获取素材接口时传入了不同的 **ProjectName**。 + * **ProjectName** 默认值为 `default`,即如果不指定该字段,则默认将资源创建至 `default` 项目中。 + * 建议在同一个项目中管理素材。 + + +#### 2. 怎样管理用户对素材库的权限? +您可使用[访问控制](https://console.volcengine.com/iam/identitymanage/user) (IAM)精细化管理用户操作素材库的权限。可按以下方式设置: + +1. **创建自定义策略** + 1. 打开[访问控制](https://console.volcengine.com/iam/policymanage) > **新建自定义策略** + 2. 输入策略名称。 + 3. 切换到 **JSON编辑器**,将下方自定义策略粘贴至编辑器中,点击 **提交** 保存。 + +
+ +```Python + +{ + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ark:*Asset*" + ], + "Resource": [ + "*" + ] + } + ] +} +``` + + + +2. **为用户/用户组赋权** + 1. 点击 **用户管理** > **用户**/**用户组**,选择需要赋权的用户或用户组,点击右侧的 **添加权限。** + 2. 在 **授权策略** 中选择**步骤 1** 中创建的策略。 + 3. (可选)在 **限制到项目资源** 中选择策略应用的项目。 + 4. 点击 **提交。** + +完成上述操作后,该用户/用户组即可在对应项目中管理素材。 +关于 IAM 的更多信息,请参考[访问控制](http://volcengine.com/docs/6257?lang=zh)。 + +#### 3. 提示词(content.**text**)中应该如何准确指代参考素材? +需在提示词输入中使用”**素材类型+序号**”格式引用素材,例如 **图片 1**、**视频 1**、**音频 1**。序号为请求体中该素材在同类素材中的排序。 +**注意**:请勿在提示词中使用 Asset ID 指代素材。 +例如,下方示例中包含 5 张参考图和 1 个参考音频,可参考示例提示词的写法引用素材。 + +* **参考:** + + + +
+
+ +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/bc3f0a1951c94cd282c690d2f8a938e0~tplv-goo7wpa0wc-image.image =426x) +图片 1 + + +
+
+ +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/c9b934d1e50246cdb840318f59e4f00a~tplv-goo7wpa0wc-image.image =157x) + +图片 2 + + +
+
+ +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/e987f41012a24a6fa8e746126916a933~tplv-goo7wpa0wc-image.image =534x) +图片 3 + + +
+
+ +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/f74e55364f664c67885761a1a02648ae~tplv-goo7wpa0wc-image.image =674x) + +图片 4 + + +
+
+ +![Image](https://p9-arcosite.byteimg.com/tos-cn-i-goo7wpa0wc/1ed8333cf28649e9a6efdef54529e436~tplv-goo7wpa0wc-image.image =574x) + +图片 5 + + +
+
+ + + +* **提示词** + +```Plain Text +清新奶油画风短剧,轻快吉他卡点快切,奶油白主色 + 蜜桃粉高光,画面柔和无特效,靠表情传情。0-2 秒:快切 2 镜图片 1中的霸总不小心撞到穿着图片 2的衣服的图片 3中的女主(两人错愕对视)+ 霸总扯下自己的西装外套披在女主身上(手部特写)」,背景吉他声起,咖啡杯掉落 / 衣服摩擦的轻柔音效;2-6 秒:快切 3 镜「女主穿霸总外套低头偷笑(脸颊泛红特写)+ 霸总看着女主背影嘴角微扬,说“我们一起走吧”参考音频 1(侧颜) + 两人在雨夜共撑一把黑伞,指尖相触快速收回(近景)」,雨天背景为图片 4,每镜卡点轻鼓重拍,配雨滴落地 / 伞骨撑开的音效,画面带轻微柔雾质感;6-8 秒:慢放两人对视笑眼,画面右下角出现图片 5的文字部分,左下角小字「NEW EP DAILY」,背景飘淡粉色花瓣(极简),BGM 落温柔尾音,画面定格两人同框侧脸。 +``` + + +* **示例代码** + +```Bash +curl --location 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks' \ + -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $ARK_API_KEY"\ + -d '{ + "model": "doubao-seedance-2-0-260128", + "content": [ + { + "type": "text", + "text": "清新奶油画风短剧,轻快吉他卡点快切,奶油白主色 + 蜜桃粉高光,画面柔和无特效,靠表情传情。0-2 秒:快切 2 镜图片 1中的霸总不小心撞到穿着图片 2的衣服的图片 3中的女主(两人错愕对视)+ 霸总扯下自己的西装外套披在女主身上(手部特写)」,背景吉他声起,咖啡杯掉落 / 衣服摩擦的轻柔音效;2-6 秒:快切 3 镜「女主穿霸总外套低头偷笑(脸颊泛红特写)+ 霸总看着女主背影嘴角微扬,说“我们一起走吧”参考音频 1(侧颜) + 两人在雨夜共撑一把黑伞,指尖相触快速收回(近景)」,雨天背景为图片 4,每镜卡点轻鼓重拍,配雨滴落地 / 伞骨撑开的音效,画面带轻微柔雾质感;6-8 秒:慢放两人对视笑眼,画面右下角出现图片 5的文字部分,左下角小字「NEW EP DAILY」,背景飘淡粉色花瓣(极简),BGM 落温柔尾音,画面定格两人同框侧脸。" + }, + { + "type": "image_url", + "role": "reference_image", + "image_url": { + "url": "asset://asset-20260224185115-hnjhb" + } + }, + { + "type": "image_url", + "role": "reference_image", + "image_url": { + "url": "asset://asset-20260224185115-8gghm" + } + }, + { + "type": "image_url", + "role": "reference_image", + "image_url": { + "url": "asset://asset-20260224185115-cjkwr" + } + }, + { + "type": "image_url", + "role": "reference_image", + "image_url": { + "url": "asset://asset-20260224185115-pxbk9" + } + }, + { + "type": "image_url", + "role": "reference_image", + "image_url": { + "url": "asset://asset-20260224185115-2c698" + } + }, + { + "type": "audio_url", + "role": "reference_audio", + "audio_url": { + "url": "asset://asset-20260224185115-dp9qm" + } + } + ], + "generate_audio": true, + "ratio": "16:9", + "duration": 11, + "watermark": false +}' +``` + diff --git a/web/src/components/RecordDetailModal.tsx b/web/src/components/RecordDetailModal.tsx index 6efa5a1..3c0e679 100644 --- a/web/src/components/RecordDetailModal.tsx +++ b/web/src/components/RecordDetailModal.tsx @@ -73,6 +73,7 @@ export function RecordDetailModal({ record: r, onClose, showTeam, showCost }: Pr + {showCost && } diff --git a/web/src/components/VideoDetailModal.tsx b/web/src/components/VideoDetailModal.tsx index f4e1b22..7e7319e 100644 --- a/web/src/components/VideoDetailModal.tsx +++ b/web/src/components/VideoDetailModal.tsx @@ -537,6 +537,8 @@ export function VideoDetailModal({ task, onClose, onReEdit, onRegenerate, onDele {task.duration}s {task.aspectRatio} + + {task.resolution.toUpperCase()} {(task.tokensConsumed ?? 0) > 0 && ( <> {(task.tokensConsumed ?? 0).toLocaleString()} tokens diff --git a/web/src/components/VideoGenerationPage.tsx b/web/src/components/VideoGenerationPage.tsx index b99080f..26f6b24 100644 --- a/web/src/components/VideoGenerationPage.tsx +++ b/web/src/components/VideoGenerationPage.tsx @@ -20,7 +20,7 @@ export function VideoGenerationPage() { const regenerate = useGenerationStore((s) => s.regenerate); const removeTask = useGenerationStore((s) => s.removeTask); const scrollRef = useRef(null); - const prevCountRef = useRef(tasks.length); + const prevLastIdRef = useRef(null); const initialLoadRef = useRef(true); const savedScrollTop = useGenerationStore((s) => s.savedScrollTop); const saveScrollPosition = useGenerationStore((s) => s.saveScrollPosition); @@ -36,9 +36,14 @@ export function VideoGenerationPage() { loadTasks(); }, [loadTasks]); - // Restore scroll position after initial load, or scroll to bottom for new tasks + // Restore scroll position after initial load, or scroll to bottom ONLY when a new task + // is appended at the tail. 通过比较末尾 task 的 id 来判断 —— 头部加载历史(prepend)、 + // 任务状态更新(如轮询完成)、删除某条都不会改变末尾 id,因此不会触发滚动, + // 避免用户往上翻时被突然拽回底部。 useEffect(() => { if (tasks.length === 0) return; + const currentLastId = tasks[tasks.length - 1]?.id ?? null; + if (initialLoadRef.current) { initialLoadRef.current = false; // Use requestAnimationFrame to ensure DOM has rendered @@ -50,15 +55,16 @@ export function VideoGenerationPage() { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }); - prevCountRef.current = tasks.length; + prevLastIdRef.current = currentLastId; return; } - if (tasks.length > prevCountRef.current && scrollRef.current) { + + if (currentLastId !== prevLastIdRef.current && scrollRef.current) { scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' }); } - prevCountRef.current = tasks.length; + prevLastIdRef.current = currentLastId; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tasks.length]); + }, [tasks]); // Save scroll position + auto-load older tasks when scrolled near top const handleScroll = useCallback(() => { diff --git a/web/src/pages/ProfilePage.tsx b/web/src/pages/ProfilePage.tsx index 59faab0..6a7acc5 100644 --- a/web/src/pages/ProfilePage.tsx +++ b/web/src/pages/ProfilePage.tsx @@ -227,6 +227,7 @@ export function ProfilePage() {
¥{(r.cost_amount || 0).toFixed(2)} + {r.resolution && {r.resolution.toUpperCase()}} {r.mode === 'universal' ? '全能参考' : '首尾帧'} {statusMap[r.status]}
diff --git a/web/src/pages/RecordsPage.tsx b/web/src/pages/RecordsPage.tsx index 526826e..239be78 100644 --- a/web/src/pages/RecordsPage.tsx +++ b/web/src/pages/RecordsPage.tsx @@ -60,7 +60,7 @@ export function RecordsPage() { team_id: teamFilter ? Number(teamFilter) : undefined, }); - const header = '任务ID,提交时间,完成时间,耗时,团队,用户名,模型,视频时长(秒),模式,比例,消费秒数,Tokens,费用(元),成本(元),利润(元),种子值,状态,提示词,失败原因,原始错误,参考素材数\n'; + const header = '任务ID,提交时间,完成时间,耗时,团队,用户名,模型,视频时长(秒),模式,比例,分辨率,消费秒数,Tokens,费用(元),成本(元),利润(元),种子值,状态,提示词,失败原因,原始错误,参考素材数\n'; const rows = data.results.map((r) => { const esc = (s: string) => s.replace(/"/g, '""').replace(/^[=+\-@]/, "'$&"); const modeLabel = r.mode === 'universal' ? '全能参考' : '首尾帧'; @@ -70,7 +70,8 @@ export function RecordsPage() { const elapsed = r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 1000) + '秒' : ''; const completedAt = r.completed_at ? new Date(r.completed_at).toLocaleString('zh-CN') : ''; const refCount = (r.reference_urls || []).length; - return `"${r.ark_task_id || ''}","${new Date(r.created_at).toLocaleString('zh-CN')}","${completedAt}","${elapsed}","${r.team_name || '-'}","${r.username}","${modelLabel}","${r.duration ?? ''}","${modeLabel}","${r.aspect_ratio || ''}","${r.seconds_consumed}","${r.tokens_consumed || 0}","${(r.cost_amount || 0).toFixed(2)}","${(r.base_cost_amount || 0).toFixed(2)}","${profit}","${r.seed != null && r.seed !== -1 ? r.seed : ''}","${statusLabel}","${esc(r.prompt || '')}","${esc(r.error_message || '')}","${esc(r.raw_error || '')}","${refCount}"`; + const resolutionLabel = r.resolution ? r.resolution.toUpperCase() : ''; + return `"${r.ark_task_id || ''}","${new Date(r.created_at).toLocaleString('zh-CN')}","${completedAt}","${elapsed}","${r.team_name || '-'}","${r.username}","${modelLabel}","${r.duration ?? ''}","${modeLabel}","${r.aspect_ratio || ''}","${resolutionLabel}","${r.seconds_consumed}","${r.tokens_consumed || 0}","${(r.cost_amount || 0).toFixed(2)}","${(r.base_cost_amount || 0).toFixed(2)}","${profit}","${r.seed != null && r.seed !== -1 ? r.seed : ''}","${statusLabel}","${esc(r.prompt || '')}","${esc(r.error_message || '')}","${esc(r.raw_error || '')}","${refCount}"`; }).join('\n'); const blob = new Blob(['\uFEFF' + header + rows], { type: 'text/csv;charset=utf-8;' }); diff --git a/web/src/pages/TeamRecordsPage.tsx b/web/src/pages/TeamRecordsPage.tsx index 9b72daf..3c736df 100644 --- a/web/src/pages/TeamRecordsPage.tsx +++ b/web/src/pages/TeamRecordsPage.tsx @@ -49,7 +49,7 @@ export function TeamRecordsPage() { end_date: endDate || undefined, }); - const header = '任务ID,提交时间,完成时间,耗时,用户名,模型,视频时长(秒),模式,比例,消费秒数,Tokens,费用(元),种子值,状态,提示词,失败原因,原始错误,参考素材数\n'; + const header = '任务ID,提交时间,完成时间,耗时,用户名,模型,视频时长(秒),模式,比例,分辨率,消费秒数,Tokens,费用(元),种子值,状态,提示词,失败原因,原始错误,参考素材数\n'; const rows = data.results.map((r) => { const esc = (s: string) => s.replace(/"/g, '""').replace(/^[=+\-@]/, "'$&"); const modeLabel = r.mode === 'universal' ? '全能参考' : '首尾帧'; @@ -58,7 +58,8 @@ export function TeamRecordsPage() { const elapsed = r.completed_at ? Math.round((new Date(r.completed_at).getTime() - new Date(r.created_at).getTime()) / 1000) + '秒' : ''; const completedAt = r.completed_at ? new Date(r.completed_at).toLocaleString('zh-CN') : ''; const refCount = (r.reference_urls || []).length; - return `"${r.ark_task_id || ''}","${new Date(r.created_at).toLocaleString('zh-CN')}","${completedAt}","${elapsed}","${r.username}","${modelLabel}","${r.duration ?? ''}","${modeLabel}","${r.aspect_ratio || ''}","${r.seconds_consumed}","${r.tokens_consumed || 0}","${(r.cost_amount || 0).toFixed(2)}","${r.seed != null && r.seed !== -1 ? r.seed : ''}","${statusLabel}","${esc(r.prompt || '')}","${esc(r.error_message || '')}","${esc(r.raw_error || '')}","${refCount}"`; + const resolutionLabel = r.resolution ? r.resolution.toUpperCase() : ''; + return `"${r.ark_task_id || ''}","${new Date(r.created_at).toLocaleString('zh-CN')}","${completedAt}","${elapsed}","${r.username}","${modelLabel}","${r.duration ?? ''}","${modeLabel}","${r.aspect_ratio || ''}","${resolutionLabel}","${r.seconds_consumed}","${r.tokens_consumed || 0}","${(r.cost_amount || 0).toFixed(2)}","${r.seed != null && r.seed !== -1 ? r.seed : ''}","${statusLabel}","${esc(r.prompt || '')}","${esc(r.error_message || '')}","${esc(r.raw_error || '')}","${refCount}"`; }).join('\n'); const blob = new Blob(['\uFEFF' + header + rows], { type: 'text/csv;charset=utf-8;' }); diff --git a/web/src/types/index.ts b/web/src/types/index.ts index f187af8..2761b8e 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -209,6 +209,7 @@ export interface AdminRecord { mode: CreationMode; model: ModelOption; aspect_ratio?: string; + resolution?: Resolution; status: 'queued' | 'processing' | 'completed' | 'failed'; error_message?: string; raw_error?: string;