393 lines
14 KiB
Python
393 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
口语-P4-看图识物 内容生产、回填、审校
|
||
生产范围:3条记录(100001, 110101, 110201)
|
||
"""
|
||
import json, subprocess, sys, os
|
||
|
||
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
|
||
TABLE_ID = "tblsD2dxaRpLmkXD"
|
||
CRED_FILE = "/root/.openclaw/credentials/xiaoyan/config.json"
|
||
|
||
# Load credentials
|
||
with open(CRED_FILE) as f:
|
||
cred = json.load(f)
|
||
APP_ID = cred['apps'][0]['appId']
|
||
APP_SECRET = cred['apps'][0]['appSecret']
|
||
|
||
def get_token():
|
||
r = subprocess.run([
|
||
"curl", "-s", "-X", "POST",
|
||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||
"-H", "Content-Type: application/json",
|
||
"-d", json.dumps({"app_id": APP_ID, "app_secret": APP_SECRET})
|
||
], capture_output=True, text=True)
|
||
return json.loads(r.stdout)['tenant_access_token']
|
||
|
||
TOKEN = get_token()
|
||
|
||
def list_records():
|
||
r = subprocess.run([
|
||
"curl", "-s", "-X", "GET",
|
||
f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records?page_size=100",
|
||
"-H", f"Authorization: Bearer {TOKEN}"
|
||
], capture_output=True, text=True)
|
||
return json.loads(r.stdout)
|
||
|
||
def create_record(fields):
|
||
body = json.dumps({"fields": fields}, ensure_ascii=False)
|
||
r = subprocess.run([
|
||
"curl", "-s", "-X", "POST",
|
||
f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records",
|
||
"-H", f"Authorization: Bearer {TOKEN}",
|
||
"-H", "Content-Type: application/json",
|
||
"-d", body
|
||
], capture_output=True, text=True)
|
||
return json.loads(r.stdout)
|
||
|
||
def get_record(record_id):
|
||
r = subprocess.run([
|
||
"curl", "-s", "-X", "GET",
|
||
f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{record_id}",
|
||
"-H", f"Authorization: Bearer {TOKEN}"
|
||
], capture_output=True, text=True)
|
||
return json.loads(r.stdout)
|
||
|
||
def build_explanation(question_text, image_desc, answer_word, is_color=False):
|
||
"""Build a detailed Chinese explanation for a question."""
|
||
if is_color:
|
||
return (
|
||
f"回答要点: {question_text}\n"
|
||
f"图片内容: {image_desc}\n"
|
||
f"考察能力: 图文匹配\n"
|
||
f"评估标准: 语音语调准确性、语言流利度、内容完整性与相关性、语法准确性\n"
|
||
f"回答指导: 鼓励学生用完整句子作答,如\"It's {answer_word}.\",根据图片颜色准确说出对应的颜色单词"
|
||
)
|
||
else:
|
||
return (
|
||
f"回答要点: {question_text}\n"
|
||
f"图片内容: {image_desc}\n"
|
||
f"考察能力: 图文匹配\n"
|
||
f"评估标准: 语音语调准确性、语言流利度、内容完整性与相关性、语法准确性\n"
|
||
f"回答指导: 鼓励学生用完整句子作答,如\"It's a {answer_word}.\",根据图片内容准确说出对应的英文单词"
|
||
)
|
||
|
||
def build_short_explanation(image_desc, answer_word, is_color=False):
|
||
"""Build short explanation for human-readable config."""
|
||
if is_color:
|
||
return f"图片中展示的颜色是{answer_word},对应的颜色单词是 {answer_word}。"
|
||
else:
|
||
return f"图片中是{answer_word},对应的英文单词是 {answer_word}。"
|
||
|
||
def build_human_config(qs_id, asr_prompt, questions, text_title="Look and answer."):
|
||
"""Build human-readable 题目配置 text."""
|
||
lines = [f"做题要求:{text_title}", "", f"热词:", asr_prompt, "", "问题:"]
|
||
for i, q in enumerate(questions, 1):
|
||
lines.append(f"{i}. ")
|
||
lines.append(f"图片:{q['imageDesc']}")
|
||
lines.append(f"题目:{q['question']}")
|
||
lines.append(f"能力:{'、'.join(q['ability'])}")
|
||
lines.append(f"解析:{q['shortExplanation']}")
|
||
lines.append("")
|
||
return "\n".join(lines)
|
||
|
||
# =============================================
|
||
# DATA DEFINITIONS
|
||
# =============================================
|
||
records_data = []
|
||
|
||
# --- ID: 100001 ---
|
||
records_data.append({
|
||
"questionSetID": "100001",
|
||
"textTitle": "Look and answer.",
|
||
"first_words": ["blue", "red", "pink", "green", "orange", "purple"],
|
||
"first_is_color": True,
|
||
"first_image_descs": [
|
||
"白色的背景,中间是一颗蓝色的星星。",
|
||
"白色的背景,中间是一个红色的气球。",
|
||
"白色的背景,中间是一朵粉红色的花。",
|
||
"白色的背景,中间是一片绿色的叶子。",
|
||
"白色的背景,中间是一个橙色的橘子。",
|
||
"白色的背景,中间是一串紫色的葡萄。",
|
||
],
|
||
"second_words": ["bag", "dress", "jacket", "hat", "T-shirt"],
|
||
"second_is_color": False,
|
||
"second_image_descs": [
|
||
"白色的背景,中间是一个书包。",
|
||
"白色的背景,中间是一条连衣裙。",
|
||
"白色的背景,中间是一件夹克衫。",
|
||
"白色的背景,中间是一顶帽子。",
|
||
"白色的背景,中间是一件T恤衫。",
|
||
],
|
||
})
|
||
|
||
# --- ID: 110101 ---
|
||
records_data.append({
|
||
"questionSetID": "110101",
|
||
"textTitle": "Look and answer.",
|
||
"first_words": ["hair", "eye", "nose", "monster", "foot", "hand"],
|
||
"first_is_color": False,
|
||
"first_image_descs": [
|
||
"白色的背景,中间是一个人的头发。",
|
||
"白色的背景,中间是一只眼睛。",
|
||
"白色的背景,中间是一个鼻子。",
|
||
"白色的背景,中间是一只可爱的小怪兽。",
|
||
"白色的背景,中间是一只脚。",
|
||
"白色的背景,中间是一只手。",
|
||
],
|
||
"second_words": ["black", "brown", "colour", "colour", "white", "yellow"],
|
||
"second_is_color": True,
|
||
"second_image_descs": [
|
||
"白色的背景,中间是一只黑色的猫。",
|
||
"白色的背景,中间是一只棕色的小狗。",
|
||
"白色的背景,中间是一个彩色的调色盘。",
|
||
"白色的背景,中间是一道彩虹。",
|
||
"白色的背景,中间是一只白色的兔子。",
|
||
"白色的背景,中间是一朵黄色的向日葵。",
|
||
],
|
||
# For colour (not a typical color answer), use "What's this?" instead of "What colour is it?"
|
||
"second_override_questions": {
|
||
2: "What's this?", # colour - 调色盘
|
||
3: "What's this?", # colour - 彩虹
|
||
},
|
||
})
|
||
|
||
# --- ID: 110201 ---
|
||
records_data.append({
|
||
"questionSetID": "110201",
|
||
"textTitle": "Look and answer.",
|
||
"first_words": ["bread", "pie", "cake", "candy", "chocolate", "ice cream"],
|
||
"first_is_color": False,
|
||
"first_image_descs": [
|
||
"白色的背景,中间是一片面包。",
|
||
"白色的背景,中间是一块馅饼。",
|
||
"白色的背景,中间是一块蛋糕。",
|
||
"白色的背景,中间是一颗糖果。",
|
||
"白色的背景,中间是一块巧克力。",
|
||
"白色的背景,中间是一个冰淇淋。",
|
||
],
|
||
"second_words": ["cat", "dog", "mice", "mouse"],
|
||
"second_is_color": False,
|
||
"second_image_descs": [
|
||
"白色的背景,中间是一只猫。",
|
||
"白色的背景,中间是一只狗。",
|
||
"白色的背景,中间是两只老鼠。",
|
||
"白色的背景,中间是一只老鼠。",
|
||
],
|
||
"second_override_questions": {
|
||
2: "What are these?", # mice (plural)
|
||
},
|
||
})
|
||
|
||
# =============================================
|
||
# GENERATE & WRITE
|
||
# =============================================
|
||
results = []
|
||
|
||
for rd in records_data:
|
||
qsid = rd["questionSetID"]
|
||
print(f"\n{'='*60}")
|
||
print(f"Processing ID: {qsid}")
|
||
print(f"{'='*60}")
|
||
|
||
# Build first question set
|
||
first_questions = []
|
||
first_words = rd["first_words"]
|
||
first_is_color = rd["first_is_color"]
|
||
first_img_descs = rd["first_image_descs"]
|
||
|
||
for i, word in enumerate(first_words):
|
||
q_text = "What colour is it?" if first_is_color else "What's this?"
|
||
q_img = f"{qsid}-{i:02d}.png"
|
||
img_desc = first_img_descs[i]
|
||
explanation = build_explanation(q_text, img_desc, word, first_is_color)
|
||
short_exp = build_short_explanation(img_desc, word, first_is_color)
|
||
first_questions.append({
|
||
"question": q_text,
|
||
"questionImage": q_img,
|
||
"imageDesc": img_desc,
|
||
"ability": ["图文匹配"],
|
||
"explanation": explanation,
|
||
"shortExplanation": short_exp,
|
||
})
|
||
|
||
first_asr = ", ".join(first_words)
|
||
|
||
first_qs = {
|
||
"category": "speaking",
|
||
"type": "speaking_pic_recognize",
|
||
"asrPrompt": first_asr,
|
||
"questionSetID": qsid,
|
||
"textTitle": rd["textTitle"],
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in first_questions],
|
||
}
|
||
|
||
# Build second question set
|
||
second_questions = []
|
||
second_words = rd["second_words"]
|
||
second_is_color = rd["second_is_color"]
|
||
second_img_descs = rd["second_image_descs"]
|
||
override_qs = rd.get("second_override_questions", {})
|
||
|
||
img_start = len(first_questions)
|
||
|
||
for i, word in enumerate(second_words):
|
||
if i in override_qs:
|
||
q_text = override_qs[i]
|
||
is_color = False
|
||
elif second_is_color:
|
||
q_text = "What colour is it?"
|
||
is_color = True
|
||
else:
|
||
q_text = "What's this?"
|
||
is_color = False
|
||
|
||
q_img = f"{qsid}-{img_start + i:02d}.png"
|
||
img_desc = second_img_descs[i]
|
||
explanation = build_explanation(q_text, img_desc, word, is_color)
|
||
short_exp = build_short_explanation(img_desc, word, is_color)
|
||
second_questions.append({
|
||
"question": q_text,
|
||
"questionImage": q_img,
|
||
"imageDesc": img_desc,
|
||
"ability": ["图文匹配"],
|
||
"explanation": explanation,
|
||
"shortExplanation": short_exp,
|
||
})
|
||
|
||
second_asr = ", ".join(second_words)
|
||
|
||
second_qs = {
|
||
"category": "speaking",
|
||
"type": "speaking_pic_recognize",
|
||
"asrPrompt": second_asr,
|
||
"questionSetID": qsid,
|
||
"textTitle": rd["textTitle"],
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in second_questions],
|
||
}
|
||
|
||
# Build jsonData
|
||
json_data = json.dumps({"first": first_qs, "second": second_qs}, ensure_ascii=False)
|
||
|
||
# Build human-readable configs
|
||
config1 = build_human_config(qsid, first_asr, first_questions, rd["textTitle"])
|
||
config2 = build_human_config(qsid, second_asr, second_questions, rd["textTitle"])
|
||
|
||
# Fields for bitable
|
||
fields = {
|
||
"dataStatus": "1",
|
||
"jsonData": json_data,
|
||
"题目1 完整配置": config1,
|
||
"题目2 完整配置": config2,
|
||
"题目集合 ID": qsid,
|
||
}
|
||
|
||
# Create record
|
||
print(f" Creating record for QSID={qsid}...")
|
||
resp = create_record(fields)
|
||
if resp.get("code") != 0:
|
||
print(f" ❌ CREATE FAILED: {resp}")
|
||
results.append({"qsid": qsid, "status": "FAILED", "error": resp})
|
||
continue
|
||
|
||
record_id = resp["data"]["record"]["record_id"]
|
||
print(f" ✅ Created: record_id={record_id}")
|
||
|
||
# Verify by reading back
|
||
print(f" Verifying...")
|
||
verify = get_record(record_id)
|
||
if verify.get("code") != 0:
|
||
print(f" ⚠️ VERIFY FAILED: {verify}")
|
||
results.append({"qsid": qsid, "record_id": record_id, "status": "VERIFY_FAILED", "error": verify})
|
||
continue
|
||
|
||
v_fields = verify["data"]["record"]["fields"]
|
||
v_json = json.loads(v_fields.get("jsonData", "{}"))
|
||
|
||
# Quick checks
|
||
checks = []
|
||
# Check first question count
|
||
f_count = len(v_json.get("first", {}).get("questionSet", []))
|
||
expected_f = len(first_words)
|
||
checks.append(("first题数", f_count == expected_f, f"{f_count}/{expected_f}"))
|
||
|
||
s_count = len(v_json.get("second", {}).get("questionSet", []))
|
||
expected_s = len(second_words)
|
||
checks.append(("second题数", s_count == expected_s, f"{s_count}/{expected_s}"))
|
||
|
||
# Check QSID
|
||
v_qsid = v_fields.get("题目集合 ID", "")
|
||
checks.append(("题目集合ID", v_qsid == qsid, v_qsid))
|
||
|
||
# Check config contains questions
|
||
c1_has = "问题:" in v_fields.get("题目1 完整配置", "")
|
||
c2_has = "问题:" in v_fields.get("题目2 完整配置", "")
|
||
checks.append(("题目1配置", c1_has, "OK" if c1_has else "MISSING"))
|
||
checks.append(("题目2配置", c2_has, "OK" if c2_has else "MISSING"))
|
||
|
||
all_ok = all(c[1] for c in checks)
|
||
status_icon = "✅" if all_ok else "⚠️"
|
||
print(f" {status_icon} Verification: {' | '.join(f'{name}={detail}' for name, ok, detail in checks)}")
|
||
|
||
results.append({
|
||
"qsid": qsid,
|
||
"record_id": record_id,
|
||
"status": "OK" if all_ok else "WARN",
|
||
"checks": checks,
|
||
})
|
||
|
||
# Print summary
|
||
print(f" First: {len(first_words)} questions, ASR: {first_asr}")
|
||
print(f" Second: {len(second_words)} questions, ASR: {second_asr}")
|
||
|
||
# =============================================
|
||
# AUDIT SUMMARY
|
||
# =============================================
|
||
print(f"\n{'='*60}")
|
||
print(f"AUDIT SUMMARY")
|
||
print(f"{'='*60}")
|
||
|
||
all_passed = True
|
||
for r in results:
|
||
if r["status"] == "OK":
|
||
print(f" ✅ {r['qsid']} | record_id={r['record_id']} | ALL CHECKS PASSED")
|
||
elif r["status"] == "WARN":
|
||
print(f" ⚠️ {r['qsid']} | record_id={r['record_id']} | WARNINGS:")
|
||
for name, ok, detail in r["checks"]:
|
||
if not ok:
|
||
print(f" - {name}: {detail}")
|
||
all_passed = False
|
||
else:
|
||
print(f" ❌ {r['qsid']} | {r['status']}: {r.get('error', '')}")
|
||
all_passed = False
|
||
|
||
# Vocabulary audit
|
||
print(f"\n--- Vocabulary Audit ---")
|
||
audit_notes = []
|
||
for rd in records_data:
|
||
qsid = rd["questionSetID"]
|
||
all_words = rd["first_words"] + rd["second_words"]
|
||
for w in all_words:
|
||
# Check: compound words, special characters
|
||
if " " in w:
|
||
audit_notes.append(f" ℹ️ {qsid}: '{w}' is multi-word — ensure ASR handles it")
|
||
if not w.isascii():
|
||
audit_notes.append(f" ⚠️ {qsid}: '{w}' contains non-ASCII")
|
||
|
||
# Check 110101 colour duplicate
|
||
audit_notes.append(f" ℹ️ 110101: 'colour' appears twice in second set — intentional per spec")
|
||
|
||
# Check 110201 mice/mouse
|
||
audit_notes.append(f" ℹ️ 110201: 'mice' (plural) and 'mouse' (singular) both present — questions use 'What are these?' vs 'What's this?'")
|
||
|
||
for note in audit_notes:
|
||
print(note)
|
||
|
||
print(f"\n{'='*60}")
|
||
if all_passed:
|
||
print("✅ ALL 3 RECORDS PRODUCED, BACKFILLED, AND VERIFIED")
|
||
else:
|
||
print("⚠️ SOME CHECKS FAILED — SEE ABOVE")
|
||
|
||
print(f"{'='*60}")
|