Install
openclaw skills install a2fArchive2Figure (a2f) skill for converting PDF archives into digital character figures. Upload PDF → Extract character features → Generate images → Retrieve results. Supports Chinese historical characters and realistic figure generation.
openclaw skills install a2fThe A2F (Archive2Figure) skill converts PDF documents containing character information into high-quality digital figure images using AI generation. It's particularly optimized for Chinese historical characters and realistic portrait generation.
Note: For ClawScan-generated or scanned PDF archives, ensure the PDF is OCR-processed or text-searchable before uploading to
archiveDatafor reliable feature extraction.
import httpx
API_BASE = "https://wuji.cyphy.com/api"
# Step 1: Upload PDF and extract features
async def extract_features(pdf_path, role_name):
async with httpx.AsyncClient(timeout=60.0) as client:
with open(pdf_path, "rb") as f:
files = {"files[0]": (pdf_path, f, "application/pdf")}
data = {
"role": role_name,
"loop_count": 2,
"gen_method": "qwen"
}
response = await client.post(f"{API_BASE}/archiveData", data=data, files=files)
return response.json()
# Step 2: Generate images
async def generate_images(prompt):
async with httpx.AsyncClient(timeout=60.0) as client:
payload = {
"text": prompt,
"negative_text": "低质量, 模糊, 变形",
"gen_method": 5,
"gen_size": 1,
"img_num": 4,
"json": 1
}
response = await client.post(f"{API_BASE}/a2fgen", json=payload)
return response.json()
# Step 3: Check job status
async def check_status(job_id):
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(f"{API_BASE}/job_status/{job_id}")
return response.json()
Endpoint: POST /api/archiveData
Upload a PDF file and extract character features and tags.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
files[0] | File | Yes | PDF file containing character information |
role | string | Yes | Character name (e.g., "李清照") |
loop_count | int | No | Extraction loops (default: 2) |
gen_method | string | No | Generation method (default: "qwen") |
openclaw | int | No | Service flag (default: 1) |
Request Example:
import httpx
async with httpx.AsyncClient() as client:
with open("character.pdf", "rb") as f:
files = {"files[0]": ("character.pdf", f, "application/pdf")}
data = {
"role": "李清照",
"loop_count": 2,
"gen_method": "qwen"
}
response = await client.post(
"https://wuji.cyphy.com/api/archiveData",
data=data,
files=files
)
result = response.json()
Response Example:
{
"李清照诗词中的自我形象研究_侯淑婉.pdf": {
"role": ["主要人物"],
"gender": ["女性"],
"age": ["成人"],
"ffeatures": ["皮肤白皙", "面容清秀", "表情细腻", "情感丰富"],
"hairstyle": ["长发", "传统发型"],
"headdress": [],
"build": ["苗条"],
"clothing": ["古代服饰", "轻薄衣衫", "色彩淡雅"],
"footwear": [],
"accessories": ["金钗", "玉饰", "首饰"],
"activity": ["写诗", "游玩", "思念"],
"background": ["宋代", "江南水乡", "封建社会", "家庭环境"],
"context": ["诗词创作", "家庭生活", "社会动荡"],
"vibe": ["婉约", "细腻", "坚韧"],
"temperament": ["敏感", "多情", "独立"],
"language": ["文言文", "宋代", "古代汉语", "文雅"],
"voice": ["柔和", "深情"],
"cusstyle": ["中国古典水墨画"]
}
}
Endpoint: POST /api/a2fgen
Generate character images using extracted features.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Combined prompt text |
negative_text | string | No | Negative prompts (quality control) |
gen_method | int | No | Generation method (default: 5) |
gen_size | int | No | Generation size (default: 1) |
img_num | int | No | Number of images (1-10, default: 4) |
json | int | No | Response format (default: 1) |
Default Negative Prompt:
文本, 特写, 裁剪, 出框, 最差质量, 低质量, jpeg 伪影, pgl y, 重复, 病态, 残缺, 额外的手指, 变异的手, 画得不好的手, 画得不好的脸, 突变, 变形, 模糊, 脱水, 不良的解剖结构, 不良的比例, 额外的肢体, 克隆的脸, 毁容, 总体比例, 畸形的四肢, 缺失的手臂, 缺失的腿, 额外的手臂, 多余的腿, 融合的手指, 太多的手指, 长脖子, 水印, 印章
Request Example:
payload = {
"text": "李清照, 女性, 成人, 肖像画, 正脸, 全身照, 站立, 看着观众, 皮肤白皙, 面容清秀, 古代服饰, 中国古典水墨画, 写实风格, 真人风格",
"negative_text": "文本, 特写, 裁剪, 出框, 最差质量, 低质量",
"gen_method": 5,
"gen_size": 1,
"img_num": 4,
"json": 1
}
async with httpx.AsyncClient() as client:
response = await client.post(
"https://wuji.cyphy.com/api/a2fgen",
json=payload
)
result = response.json()
Response Example:
{
"job_id": "12345",
"status": "processing",
"message": "Job created successfully"
}
Endpoint: GET /api/job_status/{job_id}
Check the status of an image generation job.
Parameters:
| Parameter | Type | Description |
|---|---|---|
job_id | string | Job ID from generate-images response |
Request Example:
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://wuji.cyphy.com/api/job_status/{job_id}"
)
result = response.json()
Response Example (Processing):
{
"job_id": "12345",
"status": "processing",
"progress": 50
}
Response Example (Completed):
{
"job_id": "12345",
"status": "completed",
"progress": 100,
"results": [
"https://cdn.example.com/result1.png",
"https://cdn.example.com/result2.png",
"https://cdn.example.com/result3.png",
"https://cdn.example.com/result4.png"
]
}
import httpx
import asyncio
API_BASE = "https://wuji.cyphy.com/api"
async def complete_a2f_workflow(pdf_path, role_name, style="写实风格,真人风格"):
"""Complete A2F workflow: PDF → Features → Images → Results"""
# Step 1: Extract features
print("Step 1: Extracting features from PDF...")
async with httpx.AsyncClient(timeout=60.0) as client:
with open(pdf_path, "rb") as f:
files = {"files[0]": (pdf_path, f, "application/pdf")}
data = {
"role": role_name,
"loop_count": 2,
"gen_method": "qwen"
}
response = await client.post(f"{API_BASE}/archiveData", data=data, files=files)
features_result = response.json()
# Extract features from response
pdf_key = list(features_result.keys())[0]
features = features_result[pdf_key]
print(f"Extracted features: {features['role']}, {features['gender']}")
# Step 2: Build prompt from features
print("Step 2: Building prompt and generating images...")
prompt_parts = [
role_name,
features.get("gender", [""])[0],
features.get("age", [""])[0],
"肖像画, 正脸, 全身照, 站立, 看着观众"
]
# Add detailed features
for key in ["ffeatures", "clothing", "hairstyle", "accessories"]:
if key in features and features[key]:
prompt_parts.extend(features[key])
# Add style
prompt_parts.append(style)
text = ", ".join(filter(None, prompt_parts))
# Generate images
async with httpx.AsyncClient(timeout=60.0) as client:
payload = {
"text": text,
"negative_text": "文本, 特写, 裁剪, 出框, 最差质量, 低质量, jpeg 伪影, pgl y, 重复, 病态, 残缺, 额外的手指, 变异的手, 画得不好的手, 画得不好的脸, 突变, 变形, 模糊, 脱水, 不良的解剖结构, 不良的比例, 额外的肢体, 克隆的脸, 毁容, 总体比例, 畸形的四肢, 缺失的手臂, 缺失的腿, 额外的手臂, 多余的腿, 融合的手指, 太多的手指, 长脖子, 水印, 印章",
"gen_method": 5,
"gen_size": 1,
"img_num": 4,
"json": 1
}
response = await client.post(f"{API_BASE}/a2fgen", json=payload)
gen_result = response.json()
job_id = gen_result.get("job_id") or gen_result.get("id")
print(f"Generation started. Job ID: {job_id}")
# Step 3: Poll for completion
print("Step 3: Waiting for generation to complete...")
async with httpx.AsyncClient(timeout=30.0) as client:
while True:
response = await client.get(f"{API_BASE}/job_status/{job_id}")
status_result = response.json()
status = status_result.get("status")
progress = status_result.get("progress", 0)
print(f"Status: {status} ({progress}%)")
if status == "completed":
print("\nGeneration complete!")
results = status_result.get("results", [])
for i, url in enumerate(results, 1):
print(f"Image {i}: {url}")
return results
elif status == "failed":
raise Exception(f"Generation failed: {status_result.get('error')}")
await asyncio.sleep(5)
# Usage
if __name__ == "__main__":
results = asyncio.run(complete_a2f_workflow(
"李清照诗词中的自我形象研究_侯淑婉.pdf",
"李清照"
))
The feature extraction returns multiple categories:
| Category | Description | Example Values |
|---|---|---|
role | Character role type | ["主要人物"] |
gender | Gender | ["女性"] |
age | Age range | ["成人"] |
ffeatures | Facial features | ["皮肤白皙", "面容清秀"] |
hairstyle | Hair style | ["长发", "传统发型"] |
headdress | Head accessories | [] |
build | Body type | ["苗条"] |
clothing | Clothing | ["古代服饰", "轻薄衣衫"] |
footwear | Shoes | [] |
accessories | Accessories | ["金钗", "玉饰"] |
activity | Activities | ["写诗", "游玩"] |
background | Background setting | ["宋代", "江南水乡"] |
context | Context | ["诗词创作"] |
vibe | Atmosphere | ["婉约", "细腻"] |
temperament | Personality | ["敏感", "多情"] |
language | Language style | ["文言文"] |
voice | Voice quality | ["柔和"] |
cusstyle | Custom style | ["中国古典水墨画"] |
Good Prompt Structure:
[角色名], [性别], [年龄], [基础描述], [细节特征], [服装配饰], [风格]
Example:
# Base
prompt = "李清照, 女性, 成人"
# Pose & framing
prompt += ", 肖像画, 正脸, 全身照, 站立, 看着观众"
# Features
prompt += ", 皮肤白皙, 面容清秀, 表情细腻"
# Clothing & accessories
prompt += ", 古代服饰, 轻薄衣衫, 色彩淡雅, 金钗, 玉饰"
# Style
prompt += ", 写实风格, 真人风格"
# Result:
# "李清照, 女性, 成人, 肖像画, 正脸, 全身照, 站立, 看着观众, 皮肤白皙, 面容清秀, 表情细腻, 古代服饰, 轻薄衣衫, 色彩淡雅, 金钗, 玉饰, 写实风格, 真人风格"
| Parameter | Value Range | Recommended | Effect |
|---|---|---|---|
gen_method | 1-10 | 5 | Generation algorithm |
gen_size | 1-5 | 1 | Output resolution |
img_num | 1-10 | 4 | Number of images |
Use negative prompts to control quality:
# Basic quality control
basic_negative = "低质量, 模糊, 变形, 水印"
# Detailed quality control
detailed_negative = "文本, 特写, 裁剪, 出框, 最差质量, 低质量, jpeg 伪影, pgl y, 重复, 病态, 残缺, 额外的手指, 变异的手, 画得不好的手, 画得不好的脸, 突变, 变形, 模糊, 脱水, 不良的解剖结构, 不良的比例, 额外的肢体, 克隆的脸, 毁容, 总体比例, 畸形的四肢, 缺失的手臂, 缺失的腿, 额外的手臂, 多余的腿, 融合的手指, 太多的手指, 长脖子, 水印, 印章"
{
"filename.pdf": {
"role": ["主要人物"],
"gender": ["女性"],
"age": ["成人"],
"ffeatures": ["皮肤白皙", "面容清秀"],
"hairstyle": ["长发"],
"clothing": ["古代服饰"],
"accessories": ["金钗"],
"activity": ["写诗"],
"background": ["宋代"],
"vibe": ["婉约"],
"temperament": ["敏感"],
"cusstyle": ["中国古典水墨画"]
}
}
{
"job_id": "17171",
"status": "processing",
"message": "Job created successfully"
}
{
"job_id": "17171",
"status": "processing",
"progress": 45,
"message": "Generating images..."
}
{
"job_id": "17171",
"status": "completed",
"progress": 100,
"results": [
"https://cdn.example.com/image1.png",
"https://cdn.example.com/image2.png",
"https://cdn.example.com/image3.png",
"https://cdn.example.com/image4.png"
]
}
import asyncio
import httpx
async def generate_li_qingzhao():
API_BASE = "https://wuji.cyphy.com/api"
# Extract features
async with httpx.AsyncClient() as client:
with open("李清照.pdf", "rb") as f:
files = {"files[0]": ("李清照.pdf", f, "application/pdf")}
data = {"role": "李清照", "loop_count": 2, "gen_method": "qwen", "openclaw": 1}
response = await client.post(f"{API_BASE}/archiveData", data=data, files=files)
features = response.json()
# Generate images
prompt = "李清照, 女性, 成人, 肖像画, 正脸, 全身照, 站立, 写诗, 古代服饰, 中国古典水墨画, 写实风格"
payload = {
"text": prompt,
"negative_text": "低质量, 模糊",
"gen_method": 5,
"img_num": 4,
"json": 1
}
async with httpx.AsyncClient() as client:
response = await client.post(f"{API_BASE}/a2fgen", json=payload)
job = response.json()
print(f"Job ID: {job['job_id']}")
return job['job_id']
async def batch_generate_characters(pdf_files):
"""Generate multiple characters from multiple PDFs"""
jobs = []
for pdf_file, role_name in pdf_files:
# Extract features
async with httpx.AsyncClient() as client:
with open(pdf_file, "rb") as f:
files = {"files[0]": (pdf_file, f, "application/pdf")}
data = {"role": role_name, "loop_count": 2, "gen_method": "qwen"}
response = await client.post("https://wuji.cyphy.com/api/archiveData", data=data, files=files)
features = response.json()
# Build prompt and generate
prompt = f"{role_name}, 写实风格, 真人风格"
payload = {"text": prompt, "img_num": 4, "json": 1}
async with httpx.AsyncClient() as client:
response = await client.post("https://wuji.cyphy.com/api/a2fgen", json=payload)
job = response.json()
jobs.append({"role": role_name, "job_id": job["job_id"]})
return jobs
async def generate_with_custom_style(pdf_path, role_name, style):
"""Generate character with custom style"""
# Extract features
async with httpx.AsyncClient() as client:
with open(pdf_path, "rb") as f:
files = {"files[0]": (pdf_path, f, "application/pdf")}
data = {"role": role_name, "loop_count": 2, "gen_method": "qwen"}
response = await client.post("https://wuji.cyphy.com/api/archiveData", data=data, files=files)
features = response.json()
# Custom style prompts
style_prompts = {
"anime": "动漫风格, 二次元, 动画角色",
"oil_painting": "油画风格, 古典油画, 艺术绘画",
"watercolor": "水彩风格, 水彩画, 淡雅",
"3d_render": "3D渲染, CGI, 3D角色, 立体感"
}
prompt = f"{role_name}, {style_prompts.get(style, '写实风格')}"
payload = {"text": prompt, "img_num": 4, "json": 1}
async with httpx.AsyncClient() as client:
response = await client.post("https://wuji.cyphy.com/api/a2fgen", json=payload)
return response.json()
DO:
# Good: Specific and structured
prompt = "李清照, 女性, 成人, 肖像画, 正脸, 全身照, 古代服饰, 写实风格"
DON'T:
# Bad: Too vague
prompt = "一个女人"
async def safe_generate(pdf_path, role_name):
try:
# Extract features
async with httpx.AsyncClient(timeout=60.0) as client:
with open(pdf_path, "rb") as f:
files = {"files[0]": (pdf_path, f, "application/pdf")}
data = {"role": role_name, "loop_count": 2, "gen_method": "qwen", "openclaw": 1}
response = await client.post("https://wuji.cyphy.com/api/archiveData", data=data, files=files)
response.raise_for_status()
features = response.json()
# Generate images
payload = {"text": f"{role_name}, 写实风格", "img_num": 4, "json": 1}
response = await client.post("https://wuji.cyphy.com/api/a2fgen", json=payload)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
print(f"HTTP error: {e.response.status_code}")
print(f"Response: {e.response.text}")
except Exception as e:
print(f"Error: {str(e)}")
async def poll_with_backoff(job_id, max_wait=300):
"""Poll job status with exponential backoff"""
start_time = time.time()
wait_time = 2
while time.time() - start_time < max_wait:
async with httpx.AsyncClient() as client:
response = await client.get(f"https://wuji.cyphy.com/api/job_status/{job_id}")
result = response.json()
if result["status"] == "completed":
return result
elif result["status"] == "failed":
raise Exception(f"Job failed: {result.get('error')}")
await asyncio.sleep(wait_time)
wait_time = min(wait_time * 2, 10) # Max 10 seconds
raise TimeoutError("Job timed out")
Not all features are equally important. Prioritize:
# High priority features
priority_features = [
"gender", # Essential for character appearance
"age", # Affects overall look
"ffeatures", # Facial characteristics
"clothing", # Historical accuracy
"cusstyle" # Art style
]
# Lower priority features
optional_features = [
"language", # Less relevant for visual
"voice", # Audio only
"context" # Background info
]
Issue: Feature extraction returns empty features
Issue: Generated images don't match character
Issue: Job status stays "processing" too long
Issue: "401 Unauthorized" error
Issue: Low quality images
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
httpx_logger = logging.getLogger("httpx")
httpx_logger.setLevel(logging.DEBUG)
async def test_a2f_endpoints():
"""Test A2F endpoints"""
# Test 1: Feature extraction
print("Testing feature extraction...")
# Add test code here
# Test 2: Image generation
print("Testing image generation...")
# Add test code here
# Test 3: Job status
print("Testing job status...")
# Add test code here
print("All tests passed!")