
Byted Airesearch Videoeval
- 2 installs
- 408 repo stars
- Updated August 3, 2026
- volcengine/agentkit-samples
Create and check long-running video material evaluation tasks on Volcengine, uploading videos as an internal step and fetching results asynchronously.
About
Submits video creative assets for long-running evaluation tasks and lets users query task lists and results later via bearer-token auth. A developer uses it to evaluate video materials at scale without blocking on results.
- Non-blocking: create task then query list/detail later, no auto-polling
- Upload is internal-only; up to 10 videos per task with rolling free quota
Byted Airesearch Videoeval by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,842 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/volcengine/agentkit-samples --skill byted-airesearch-videoevalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 408 |
| Last updated | August 3, 2026 |
| Repository | volcengine/agentkit-samples ↗ |
What it does
Create and check long-running video material evaluation tasks on Volcengine, uploading videos as an internal step and fetching results asynchronously.
Files
Byted Airesearch Videoeval
Use this skill to submit and query long-running material evaluation tasks.
When to use
Use this skill when the user wants to:
- evaluate a video material or creative asset
- submit a material evaluation task and get back a task identifier
- check the status of an existing evaluation task later
- fetch the final detail/result of a previously created evaluation task
Do not use this skill for generic video upload requests.
Current workflow
1. Validate the full input batch before any upload starts. 2. Upload the local video files and capture the returned attachment_id values. 3. Create a task with the uploaded attachment IDs. 4. Return success immediately after the task is created. 5. Ask the user to query task list or task detail later if they want progress or results.
This workflow is intentionally non-blocking. Do not poll automatically after task creation.
Mandatory behavior
- Do not expose the upload API as a standalone user-facing capability.
- Do not trigger this skill for generic requests such as “upload this video”, “store this file”, or “send this video”.
- Only call the upload API when the user explicitly intends to create a new material evaluation task.
- For new task creation, prefer
scripts/submit_evaluation_task.pyso validation, upload, and task creation stay in one controlled flow. - Treat
scripts/upload_video.pyas an internal helper used by the orchestration flow, not as the primary user entrypoint. The script itself rejects direct use unless it is called with the internal orchestration marker.
For multi-file submissions, use the orchestration entrypoint so the whole batch is validated before the first upload starts.
Submission limits
- A single task can include at most 10 videos.
- Non-enabled users have a rolling free quota of at most 10 submitted videos within the last 24 hours.
- The new task's video count is added to the number of videos already submitted in the last 24 hours. If the total exceeds 10, the service rejects the task and asks the user to contact Volcengine sales to enable access.
- Enabled users are not restricted by this rolling 24-hour free quota.
- Quota accounting is based on the actual number of videos submitted per task, with no deduplication.
- Any task created within the last 24 hours counts toward the rolling quota, including running tasks.
- Login-based access and API key access share the same quota pool.
- The skill enforces the per-task limit locally before upload starts. The rolling 24-hour quota is enforced by the service, and the skill should surface the service rejection with a clear explanation.
Authentication
The current APIs use API key authentication.
All API requests sent by this skill must include the header:
x-product-version: 20Authorization: bearer {API_KEY}
Preferred input methods:
--api-key "<api-key>"BYTED_AIRESEARCH_VIDEOEVAL_API_KEY
If no API key is available, ask the user to create or view one at:
https://console.volcengine.com/datatester/ai-research/audience/list?tab=apikey
Then ask the user to provide the API key before calling the API.
If the API key is missing, the scripts must fail immediately with a clear error that points the user to the API key page above.
Known API coverage
Upload attachment
- Endpoint:
POST https://console.volcengine.com/datatester/compass/api/v3/survey/attachment - Request:
multipart/form-data - File field:
file - Upload constraints:
- each upload request contains exactly one video file
- each video must be 50MB or smaller
- file format must be
mp4 - MIME type must be
video/mp4 - Output mapping:
- preserve the raw attachment payload
- map the attachment object's
idfield toattachment_id
The upload_video.py script is an internal helper for the create-task workflow. It is not the primary user-facing entrypoint.
Create task
- Endpoint:
POST https://console.volcengine.com/datatester/compass/api/v3/survey/task - Fixed request fields:
form_id: 0agent_id: 125audience_id: 3664529- Optional request fields currently exposed:
promptlanguageis_typical_user_enabledtypical_user_counttypical_user_selection_modeis_report_enabledattachment_ids- Create constraints:
- one task submission can include at most 10 videos
attachment_idsmust contain at most 10 items per request
The create step should send the uploaded attachment_id as a one-element attachment_ids array unless multiple attachment IDs are explicitly provided.
List and detail
Task detail is wired and can query an existing task directly:
- Endpoint:
GET https://console.volcengine.com/datatester/compass/api/v3/survey/task/{id} - Current auth: API key bearer token
- Output mapping:
- map the task object's
idfield totask_id - map the task object's
statusfield totask_status - parse
task.detail - keep only items where
key == video_structured_resultandsub_tab != null - expose a compact
summaryblock for downstream agent use
Task list is wired and can query existing tasks directly:
- Endpoint:
GET https://console.volcengine.com/datatester/compass/api/v3/survey/task - Current auth: API key bearer token
- Current fixed query params:
page=1page_size=100agent_id=125- Output mapping:
- map each task item to
task_id,name,status,created_at,updated_at - preserve pagination info in
data.page
Commands
# Validate a whole batch before upload, then upload all files and create the task
python scripts/submit_evaluation_task.py \
--file /path/to/video-1.mp4 \
--file /path/to/video-2.mp4 \
--api-key "<api-key>"
# Single-video create flow
python scripts/submit_evaluation_task.py \
--file /path/to/video.mp4 \
--prompt "Evaluate this material for audience fit and content quality." \
--api-key "<api-key>"
# Query task list later
python scripts/list_evaluation_tasks.py
# Query task detail later
python scripts/get_evaluation_task_detail.py --task-id 12345Response handling
All scripts emit JSON to stdout with the same top-level envelope:
statusmessagerequest_iddataerror
Important normalized fields:
- submit task:
data.task_id,data.task_status,data.submitted_video_count - list:
data.items - detail:
data.detail,data.summary
Final answer rules
- Use the structured JSON returned by the detail endpoint as the internal source of truth.
- Present the final answer in human-readable natural language.
- When the task is finished, prefer a concise report-style answer rather than a raw data dump.
- Do not dump raw JSON to the user.
- Do not expose internal field names such as
video_eval,video_user_report,distribution,field_desc, or similar implementation-oriented keys. - Apply the same rule to task list responses: use the list result as internal source data, but present the outcome as a natural-language summary rather than raw fields.
- For task list answers, it is acceptable to include the task ID, task name, status, created time, and updated time in human-readable prose, because those fields help the user choose a task for follow-up detail queries.
- When multiple videos are present, summarize them separately.
- For finished task detail results, prefer a readable report flow such as: task conclusion first, then one short section per video, then overall recommendations if the source data supports them.
- If the task is not finished yet, do not fabricate a report. Clearly state the current status and ask the user to check again later.
- Only provide raw structured data if the user explicitly asks for the raw result.
Practical guidance
- For new submissions, use the orchestration flow rather than exposing upload as a standalone step to the user.
- Validate the local file before upload. Reject non-MP4 files, files with non-
video/mp4MIME types, or files larger than 50MB with a direct and actionable error message. - Validate the task creation input before calling the API. Reject any request that contains more than 10 attachment IDs with a direct and actionable error message.
- For a multi-video submit flow, validate the full batch size before any upload starts. If the batch contains more than 10 files, fail immediately and do not upload anything.
- If the service rejects task creation because the rolling 24-hour free quota was exceeded, use this standard Chinese wording for the user-facing message:
免费版用户每24小时最多提交10个评估视频,如需购买请联系火山引擎销售人员 - After create succeeds, tell the user the task has been submitted successfully and can be checked later.
- Use task list when the user wants to browse or find historical tasks.
- Use task detail when the user already knows the task ID and wants the final result.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared helpers for the byted-airesearch-videoeval skill."""
from __future__ import annotations
import json
import os
import sys
import uuid
from typing import Any, Optional
import requests
BASE_URL_ENV = "BYTED_AIRESEARCH_VIDEOEVAL_BASE_URL"
BASE_URL = os.getenv(BASE_URL_ENV, "https://console.volcengine.com").rstrip("/")
UPLOAD_PATH = "/datatester/compass/api/v3/survey/attachment"
CREATE_TASK_PATH = "/datatester/compass/api/v3/survey/task"
API_KEY_ENV = "BYTED_AIRESEARCH_VIDEOEVAL_API_KEY"
DEFAULT_TIMEOUT = 60.0
PRODUCT_VERSION_HEADER = "x-product-version"
PRODUCT_VERSION_VALUE = "20"
def build_request_id() -> str:
return str(uuid.uuid4())
def success(message: str, data: Any, request_id: Optional[str] = None) -> dict[str, Any]:
return {
"status": "success",
"message": message,
"request_id": request_id or build_request_id(),
"data": data,
"error": None,
}
def failure(
message: str,
code: str,
details: Any = None,
request_id: Optional[str] = None,
) -> dict[str, Any]:
return {
"status": "error",
"message": message,
"request_id": request_id or build_request_id(),
"data": None,
"error": {"code": code, "details": details},
}
def print_json(payload: dict[str, Any]) -> None:
print(json.dumps(payload, ensure_ascii=False, indent=2))
def resolve_api_key(api_key: Optional[str]) -> str:
resolved = (api_key or os.getenv(API_KEY_ENV, "")).strip()
if resolved:
return resolved
raise PermissionError(
"An API key is required. Provide --api-key or set "
f"{API_KEY_ENV}. If you do not have one yet, create or view an API key at "
"https://console.volcengine.com/datatester/ai-research/audience/list?tab=apikey first."
)
def build_headers(api_key: Optional[str] = None) -> dict[str, str]:
headers: dict[str, str] = {
"Accept": "application/json",
"User-Agent": "byted-airesearch-videoeval/0.1",
PRODUCT_VERSION_HEADER: PRODUCT_VERSION_VALUE,
}
resolved_api_key = resolve_api_key(api_key)
headers["Authorization"] = f"bearer {resolved_api_key}"
return headers
def build_url(path: str) -> str:
if path.startswith("http://") or path.startswith("https://"):
return path
return f"{BASE_URL}{path}"
def create_session(api_key: Optional[str] = None) -> requests.Session:
session = requests.Session()
session.headers.update(build_headers(api_key))
return session
def parse_json_response(response: requests.Response) -> Any:
try:
return response.json()
except ValueError as exc:
raise ValueError(
f"Response is not valid JSON. status={response.status_code}, body={response.text[:1000]}"
) from exc
def extract_business_error(payload: Any) -> tuple[Optional[str], Optional[str]]:
if not isinstance(payload, dict):
return None, None
code = payload.get("code")
message = payload.get("message")
if not isinstance(message, str) or not message.strip():
return None, None
if isinstance(code, int):
if code != 0:
return str(code), message
return None, None
if isinstance(code, str):
normalized = code.strip()
if not normalized or normalized in {"0", "success", "ok", "OK", "SUCCESS"}:
return None, None
return normalized, message
return None, None
def is_quota_exceeded_message(message: Optional[str]) -> bool:
if not message:
return False
normalized = message.lower()
return "free usage limit" in normalized or "24 hours" in normalized
def format_quota_exceeded_message(message: str) -> str:
return (
"免费版用户每24小时最多提交10个评估视频,如需购买请联系火山引擎销售人员。 "
f"Service message: {message}"
)
def unwrap_payload(payload: Any) -> Any:
if isinstance(payload, dict):
for key in ("data", "result", "payload"):
value = payload.get(key)
if value is not None:
return value
return payload
def exit_with_payload(payload: dict[str, Any], status_code: int = 0) -> None:
print_json(payload)
raise SystemExit(status_code)
def not_implemented_payload(operation: str) -> dict[str, Any]:
return failure(
message=f"{operation} API is not wired yet.",
code="NOT_IMPLEMENTED",
details={
"operation": operation,
"base_url": BASE_URL,
"note": "Update this script after the concrete API definition is provided.",
},
)
def normalize_attachment(payload: Any) -> tuple[Optional[str], Any]:
candidate = unwrap_payload(payload)
if isinstance(candidate, dict) and "id" in candidate:
return str(candidate["id"]), candidate
if isinstance(payload, dict):
for key in ("attachment", "item"):
value = payload.get(key)
if isinstance(value, dict) and "id" in value:
return str(value["id"]), value
return None, candidate
def normalize_task(payload: Any) -> tuple[Optional[str], Optional[str], Any]:
candidate = unwrap_payload(payload)
if isinstance(candidate, dict):
task_id = candidate.get("id")
status = candidate.get("status")
if task_id is not None:
return str(task_id), None if status is None else str(status), candidate
if isinstance(payload, dict):
for key in ("task", "item"):
value = payload.get(key)
if isinstance(value, dict) and value.get("id") is not None:
status = value.get("status")
return str(value["id"]), None if status is None else str(status), value
return None, None, candidate
def request_error_details(response: requests.Response, body: Any) -> dict[str, Any]:
return {
"status_code": response.status_code,
"response": body,
}
def ensure_requests_available() -> None:
if requests is None:
raise RuntimeError("requests is required")
def handle_top_level_exception(exc: Exception) -> None:
payload = failure(
message=str(exc),
code=exc.__class__.__name__.upper(),
)
print_json(payload)
raise SystemExit(1)
def validate_file_exists(path: str) -> str:
if not path:
raise ValueError("A file path is required.")
if not os.path.exists(path):
raise FileNotFoundError(f"File does not exist: {path}")
if not os.path.isfile(path):
raise ValueError(f"Path is not a file: {path}")
return path
def load_binary_file(path: str) -> bytes:
with open(path, "rb") as file_obj:
return file_obj.read()
def stderr(message: str) -> None:
print(message, file=sys.stderr)
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Create a long-running material evaluation task."""
from __future__ import annotations
import argparse
from typing import Any
from common import (
CREATE_TASK_PATH,
DEFAULT_TIMEOUT,
build_request_id,
build_url,
create_session,
extract_business_error,
failure,
format_quota_exceeded_message,
handle_top_level_exception,
is_quota_exceeded_message,
normalize_task,
parse_json_response,
print_json,
request_error_details,
success,
)
FIXED_AGENT_ID = 125
FIXED_AUDIENCE_ID = 3664529
FIXED_FORM_ID = 0
VALID_LANGUAGES = ("auto", "zh", "en")
VALID_TYPICAL_USER_SELECTION_MODES = ("VIEWPOINT", "PROFILE")
MAX_ATTACHMENT_IDS = 10
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Create a material evaluation task."
)
parser.add_argument(
"--attachment-id",
action="append",
required=True,
help="Attachment ID returned by upload_video.py. Repeat to send multiple IDs.",
)
parser.add_argument("--prompt", help="Prompt text for task creation.")
parser.add_argument(
"--language",
choices=VALID_LANGUAGES,
help="Language preference for the evaluation task.",
)
parser.add_argument(
"--enable-typical-user",
action="store_true",
help="Enable typical user simulation.",
)
parser.add_argument(
"--typical-user-count",
type=int,
help="Typical user count when typical user simulation is enabled.",
)
parser.add_argument(
"--typical-user-selection-mode",
choices=VALID_TYPICAL_USER_SELECTION_MODES,
help="Selection mode for typical user generation.",
)
parser.add_argument(
"--enable-report",
action="store_true",
help="Enable report generation.",
)
parser.add_argument(
"--api-key",
help="API key used to build the Authorization header.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser
def build_request_body(args: argparse.Namespace) -> dict[str, Any]:
attachment_ids = args.attachment_id
if len(attachment_ids) > MAX_ATTACHMENT_IDS:
raise ValueError(
"Too many attachment IDs. "
f"At most {MAX_ATTACHMENT_IDS} attachment_ids are allowed per task creation request, "
f"got {len(attachment_ids)}."
)
body: dict[str, Any] = {
"agent_id": FIXED_AGENT_ID,
"audience_id": FIXED_AUDIENCE_ID,
"form_id": FIXED_FORM_ID,
"attachment_ids": attachment_ids,
}
if args.prompt:
body["prompt"] = args.prompt
if args.language:
body["language"] = args.language
if args.enable_typical_user:
body["is_typical_user_enabled"] = True
if args.typical_user_count is not None:
if args.typical_user_count <= 0:
raise ValueError("--typical-user-count must be greater than 0.")
body["typical_user_count"] = args.typical_user_count
if args.typical_user_selection_mode:
body["typical_user_selection_mode"] = args.typical_user_selection_mode
if args.enable_report:
body["is_report_enabled"] = True
return body
def create_task(body: dict[str, Any], api_key: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> dict:
request_id = build_request_id()
session = create_session(api_key)
response = session.post(build_url(CREATE_TASK_PATH), json=body, timeout=timeout)
raw_body = parse_json_response(response)
business_code, business_message = extract_business_error(raw_body)
if response.status_code >= 400 or business_code is not None:
message = "Task creation failed."
if business_message:
message = (
format_quota_exceeded_message(business_message)
if is_quota_exceeded_message(business_message)
else f"Task creation failed. Service message: {business_message}"
)
return failure(
message=message,
code="CREATE_TASK_FAILED",
details=request_error_details(response, raw_body),
request_id=request_id,
)
task_id, task_status, task = normalize_task(raw_body)
if not task_id:
return failure(
message="Task creation succeeded but no task ID could be extracted.",
code="TASK_ID_MISSING",
details={"response": raw_body},
request_id=request_id,
)
return success(
message="Evaluation task created successfully.",
data={
"task_id": task_id,
"task_status": task_status,
"follow_up_hint": (
"This is a long-running task. Query task list or task detail later to check progress or fetch the result."
),
},
request_id=request_id,
)
def main() -> None:
parser = build_parser()
args = parser.parse_args()
body = build_request_body(args)
payload = create_task(body=body, api_key=args.api_key, timeout=args.timeout)
print_json(payload)
if __name__ == "__main__":
try:
main()
except Exception as exc:
handle_top_level_exception(exc)
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Get material evaluation task detail."""
from __future__ import annotations
import argparse
import json
from typing import Any
from common import (
CREATE_TASK_PATH,
DEFAULT_TIMEOUT,
build_request_id,
build_url,
create_session,
failure,
handle_top_level_exception,
normalize_task,
parse_json_response,
print_json,
request_error_details,
success,
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Get material evaluation task detail."
)
parser.add_argument("--task-id", required=True, help="Task identifier.")
parser.add_argument(
"--api-key",
help="API key used to build the Authorization header.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser
def build_summary(task: dict) -> dict:
return {
"task_id": task.get("id"),
"status": task.get("status"),
"name": task.get("name"),
"prompt": task.get("prompt"),
"result": task.get("result"),
"updated_at": task.get("updated_at"),
}
def parse_detail(detail: Any) -> list[dict[str, Any]]:
if detail in (None, ""):
return []
if isinstance(detail, list):
parsed = detail
elif isinstance(detail, str):
parsed = json.loads(detail)
else:
raise ValueError("Task detail must be a JSON string or a list.")
if not isinstance(parsed, list):
raise ValueError("Parsed task detail is not a list.")
return [item for item in parsed if isinstance(item, dict)]
def filter_summary_report_items(detail_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
item
for item in detail_items
if item.get("key") == "video_structured_result" and item.get("sub_tab") is not None
]
def get_task_detail(task_id: str, api_key: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> dict:
request_id = build_request_id()
session = create_session(api_key)
response = session.get(build_url(f"{CREATE_TASK_PATH}/{task_id}"), timeout=timeout)
raw_body = parse_json_response(response)
if response.status_code >= 400:
return failure(
message="Task detail request failed.",
code="GET_TASK_DETAIL_FAILED",
details=request_error_details(response, raw_body),
request_id=request_id,
)
normalized_task_id, task_status, task = normalize_task(raw_body)
if not normalized_task_id:
return failure(
message="Task detail request succeeded but no task ID could be extracted.",
code="TASK_ID_MISSING",
details={"response": raw_body},
request_id=request_id,
)
if not isinstance(task, dict):
return failure(
message="Task detail response is not a task object.",
code="INVALID_TASK_PAYLOAD",
details={"response": raw_body},
request_id=request_id,
)
detail_items = parse_detail(task.get("detail"))
filtered_detail = filter_summary_report_items(detail_items)
return success(
message="Task detail fetched successfully.",
data={
"task_id": normalized_task_id,
"task_status": task_status,
"detail": filtered_detail,
"filtered_detail_count": len(filtered_detail),
"summary": build_summary(task),
},
request_id=request_id,
)
def main() -> None:
args = build_parser().parse_args()
payload = get_task_detail(
task_id=args.task_id,
api_key=args.api_key,
timeout=args.timeout,
)
print_json(payload)
if __name__ == "__main__":
try:
main()
except Exception as exc:
handle_top_level_exception(exc)
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""List material evaluation tasks."""
from __future__ import annotations
import argparse
from common import (
CREATE_TASK_PATH,
DEFAULT_TIMEOUT,
build_request_id,
build_url,
create_session,
failure,
handle_top_level_exception,
parse_json_response,
print_json,
request_error_details,
success,
)
FIXED_PAGE = 1
FIXED_PAGE_SIZE = 100
FIXED_AGENT_ID = 125
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="List material evaluation tasks."
)
parser.add_argument(
"--api-key",
help="API key used to build the Authorization header.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser
def build_query_params() -> dict[str, int]:
return {
"page": FIXED_PAGE,
"page_size": FIXED_PAGE_SIZE,
"agent_id": FIXED_AGENT_ID,
}
def normalize_list_payload(raw_body: object) -> tuple[list[dict], dict | None]:
if not isinstance(raw_body, dict):
raise ValueError("Task list response is not a JSON object.")
payload = raw_body.get("data", raw_body)
if not isinstance(payload, dict):
raise ValueError("Task list payload is not a JSON object.")
items = payload.get("list", [])
page = payload.get("page")
if not isinstance(items, list):
raise ValueError("Task list field 'list' is not an array.")
if page is not None and not isinstance(page, dict):
raise ValueError("Task list field 'page' is not an object.")
normalized_items = []
for item in items:
if not isinstance(item, dict):
continue
normalized_items.append(
{
"task_id": item.get("id"),
"name": item.get("name"),
"status": item.get("status"),
"created_at": item.get("created_at"),
"updated_at": item.get("updated_at"),
}
)
return normalized_items, page
def list_tasks(api_key: str | None = None, timeout: float = DEFAULT_TIMEOUT) -> dict:
request_id = build_request_id()
session = create_session(api_key)
response = session.get(
build_url(CREATE_TASK_PATH),
params=build_query_params(),
timeout=timeout,
)
raw_body = parse_json_response(response)
if response.status_code >= 400:
return failure(
message="Task list request failed.",
code="LIST_TASKS_FAILED",
details=request_error_details(response, raw_body),
request_id=request_id,
)
items, page = normalize_list_payload(raw_body)
return success(
message="Task list fetched successfully.",
data={
"items": items,
"page": page,
},
request_id=request_id,
)
def main() -> None:
args = build_parser().parse_args()
payload = list_tasks(api_key=args.api_key, timeout=args.timeout)
print_json(payload)
if __name__ == "__main__":
try:
main()
except Exception as exc:
handle_top_level_exception(exc)
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Upload one or more videos, then create a material evaluation task."""
from __future__ import annotations
import argparse
from common import handle_top_level_exception, print_json
from create_evaluation_task import MAX_ATTACHMENT_IDS, build_request_body, create_task
from upload_video import INTERNAL_USE_TOKEN, upload_video, validate_upload_constraints
from common import DEFAULT_TIMEOUT, validate_file_exists
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Upload one or more videos, then create a material evaluation task."
)
parser.add_argument(
"--file",
action="append",
required=True,
help="Local MP4 file path to upload. Repeat to submit multiple videos.",
)
parser.add_argument("--prompt", help="Prompt text for task creation.")
parser.add_argument(
"--language",
choices=("auto", "zh", "en"),
help="Language preference for the evaluation task.",
)
parser.add_argument(
"--enable-typical-user",
action="store_true",
help="Enable typical user simulation.",
)
parser.add_argument(
"--typical-user-count",
type=int,
help="Typical user count when typical user simulation is enabled.",
)
parser.add_argument(
"--typical-user-selection-mode",
choices=("VIEWPOINT", "PROFILE"),
help="Selection mode for typical user generation.",
)
parser.add_argument(
"--enable-report",
action="store_true",
help="Enable report generation.",
)
parser.add_argument(
"--api-key",
help="API key used to build the Authorization header.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser
def validate_batch(file_paths: list[str]) -> None:
if len(file_paths) > MAX_ATTACHMENT_IDS:
raise ValueError(
"Too many videos. "
f"At most {MAX_ATTACHMENT_IDS} videos are allowed per task submission, got {len(file_paths)}."
)
for file_path in file_paths:
validate_file_exists(file_path)
validate_upload_constraints(file_path)
def submit_task(args: argparse.Namespace) -> dict:
validate_batch(args.file)
attachment_ids: list[str] = []
for file_path in args.file:
upload_payload = upload_video(
file_path=file_path,
api_key=args.api_key,
timeout=args.timeout,
internal_use_only=INTERNAL_USE_TOKEN,
)
if upload_payload["status"] != "success":
return upload_payload
upload_data = upload_payload["data"]
attachment_ids.append(upload_data["attachment_id"])
body = build_request_body(
argparse.Namespace(
attachment_id=attachment_ids,
prompt=args.prompt,
language=args.language,
enable_typical_user=args.enable_typical_user,
typical_user_count=args.typical_user_count,
typical_user_selection_mode=args.typical_user_selection_mode,
enable_report=args.enable_report,
)
)
task_payload = create_task(body=body, api_key=args.api_key, timeout=args.timeout)
if task_payload["status"] != "success":
return task_payload
task_payload["data"]["submitted_video_count"] = len(attachment_ids)
return task_payload
def main() -> None:
args = build_parser().parse_args()
payload = submit_task(args)
print_json(payload)
if __name__ == "__main__":
try:
main()
except Exception as exc:
handle_top_level_exception(exc)
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Internal helper to upload a local video attachment for material evaluation."""
from __future__ import annotations
import argparse
import mimetypes
import os
from common import (
CREATE_TASK_PATH,
DEFAULT_TIMEOUT,
UPLOAD_PATH,
build_request_id,
build_url,
create_session,
failure,
handle_top_level_exception,
normalize_attachment,
parse_json_response,
print_json,
request_error_details,
success,
validate_file_exists,
)
MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024
ALLOWED_EXTENSION = ".mp4"
ALLOWED_MIME_TYPE = "video/mp4"
INTERNAL_USE_TOKEN = "create-task-flow"
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Internal helper: upload a local video to the material evaluation attachment API."
)
parser.add_argument("--file", required=True, help="Local file path to upload.")
parser.add_argument(
"--internal-use-only",
help="Internal marker required for create-task orchestration.",
)
parser.add_argument(
"--api-key",
help="API key used to build the Authorization header.",
)
parser.add_argument(
"--timeout",
type=float,
default=DEFAULT_TIMEOUT,
help=f"HTTP timeout in seconds. Default: {DEFAULT_TIMEOUT}.",
)
return parser
def validate_internal_usage(internal_use_only: str | None) -> None:
if internal_use_only != INTERNAL_USE_TOKEN:
raise PermissionError(
"Direct upload is not allowed. "
"The upload API can only be used inside the material evaluation task creation flow."
)
def validate_upload_constraints(file_path: str) -> None:
extension = os.path.splitext(file_path)[1].lower()
if extension != ALLOWED_EXTENSION:
raise ValueError(
"Only MP4 videos are supported. "
f"Expected a '{ALLOWED_EXTENSION}' file, got '{extension or 'no extension'}'."
)
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type != ALLOWED_MIME_TYPE:
raise ValueError(
"Invalid MIME type for upload. "
f"Expected '{ALLOWED_MIME_TYPE}', got '{mime_type or 'unknown'}'."
)
file_size = os.path.getsize(file_path)
if file_size > MAX_FILE_SIZE_BYTES:
size_mb = file_size / (1024 * 1024)
raise ValueError(
"Video file is too large. "
f"Each video must be 50MB or smaller, got {size_mb:.2f}MB."
)
def upload_video(
file_path: str,
api_key: str | None = None,
timeout: float = DEFAULT_TIMEOUT,
internal_use_only: str | None = None,
) -> dict:
validate_internal_usage(internal_use_only)
validate_file_exists(file_path)
validate_upload_constraints(file_path)
request_id = build_request_id()
session = create_session(api_key)
url = build_url(UPLOAD_PATH)
with open(file_path, "rb") as file_obj:
files = {
"file": (os.path.basename(file_path), file_obj, ALLOWED_MIME_TYPE)
}
response = session.post(url, files=files, timeout=timeout)
body = parse_json_response(response)
if response.status_code >= 400:
return failure(
message="Attachment upload failed.",
code="UPLOAD_REQUEST_FAILED",
details=request_error_details(response, body),
request_id=request_id,
)
attachment_id, attachment = normalize_attachment(body)
if not attachment_id:
return failure(
message="Attachment upload succeeded but no attachment ID could be extracted.",
code="ATTACHMENT_ID_MISSING",
details={"response": body},
request_id=request_id,
)
return success(
message="Attachment uploaded successfully.",
data={
"attachment_id": attachment_id,
"attachment": attachment,
"raw_response": body,
"follow_up_hint": (
"Use this attachment_id in create_evaluation_task.py as --attachment-id."
),
"known_create_endpoint": build_url(CREATE_TASK_PATH),
},
request_id=request_id,
)
def main() -> None:
parser = build_parser()
args = parser.parse_args()
payload = upload_video(
file_path=args.file,
api_key=args.api_key,
timeout=args.timeout,
internal_use_only=args.internal_use_only,
)
print_json(payload)
if __name__ == "__main__":
try:
main()
except Exception as exc:
handle_top_level_exception(exc)