296 lines
12 KiB
Python
296 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
口语-P4-看图识物 调整:统一5题/组 + 审校结果写入
|
||
"""
|
||
import json, subprocess
|
||
|
||
TOKEN = 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':'cli_a931175d41799cc7','app_secret':'Iw2vEfbjT6GtV0GhbxbZqfQ4nAPtbR14'})
|
||
], capture_output=True, text=True)
|
||
TOKEN = json.loads(TOKEN.stdout)['tenant_access_token']
|
||
|
||
APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf"
|
||
TABLE_ID = "tblsD2dxaRpLmkXD"
|
||
|
||
def update_record(rid, fields):
|
||
body = json.dumps({"fields": fields}, ensure_ascii=False)
|
||
r = subprocess.run([
|
||
'curl', '-s', '-X', 'PUT',
|
||
f'https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}',
|
||
'-H', f'Authorization: Bearer {TOKEN}',
|
||
'-H', 'Content-Type: application/json',
|
||
'-d', body
|
||
], capture_output=True, text=True)
|
||
return json.loads(r.stdout)
|
||
|
||
def build_explanation(q_text, img_desc, word, is_color):
|
||
if is_color:
|
||
return (
|
||
f"回答要点: {q_text}\n"
|
||
f"图片内容: {img_desc}\n"
|
||
f"考察能力: 图文匹配\n"
|
||
f"评估标准: 语音语调准确性、语言流利度、内容完整性与相关性、语法准确性\n"
|
||
f"回答指导: 鼓励学生用完整句子作答,如\"It's {word}.\",根据图片颜色准确说出对应的颜色单词"
|
||
)
|
||
else:
|
||
return (
|
||
f"回答要点: {q_text}\n"
|
||
f"图片内容: {img_desc}\n"
|
||
f"考察能力: 图文匹配\n"
|
||
f"评估标准: 语音语调准确性、语言流利度、内容完整性与相关性、语法准确性\n"
|
||
f"回答指导: 鼓励学生用完整句子作答,如\"It's a {word}.\",根据图片内容准确说出对应的英文单词"
|
||
)
|
||
|
||
def build_short_exp(img_desc, word, is_color):
|
||
obj = img_desc.replace('白色的背景,', '').rstrip('。')
|
||
if is_color:
|
||
return f'{obj},对应的颜色单词是 {word}。'
|
||
else:
|
||
return f'{obj},对应的英文单词是 {word}。'
|
||
|
||
def build_human_config(qsid, asr_prompt, questions, text_title="Look and answer."):
|
||
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)
|
||
|
||
def build_qs(qsid, words, image_descs, question_types, img_start):
|
||
"""question_types: list of (is_color_bool, question_text_override_or_None)"""
|
||
qs = []
|
||
for i, word in enumerate(words):
|
||
is_color, q_override = question_types[i] if i < len(question_types) else (False, None)
|
||
if q_override:
|
||
q_text = q_override
|
||
is_color = False
|
||
elif is_color:
|
||
q_text = "What colour is it?"
|
||
else:
|
||
q_text = "What's this?"
|
||
|
||
q_img = f"{qsid}-{img_start + i:02d}.png"
|
||
img_desc = image_descs[i]
|
||
explanation = build_explanation(q_text, img_desc, word, is_color)
|
||
short_exp = build_short_exp(img_desc, word, is_color)
|
||
qs.append({
|
||
"question": q_text,
|
||
"questionImage": q_img,
|
||
"imageDesc": img_desc,
|
||
"ability": ["图文匹配"],
|
||
"explanation": explanation,
|
||
"shortExplanation": short_exp,
|
||
})
|
||
return qs
|
||
|
||
# ============================================================
|
||
# ID: 100001 — first=6→5 (drop purple), second=5 (no change)
|
||
# ============================================================
|
||
qsid = "100001"
|
||
first_words = ["blue", "red", "pink", "green", "orange"]
|
||
first_descs = [
|
||
"白色的背景,中间是一颗蓝色的星星。",
|
||
"白色的背景,中间是一个红色的气球。",
|
||
"白色的背景,中间是一朵粉红色的花。",
|
||
"白色的背景,中间是一片绿色的叶子。",
|
||
"白色的背景,中间是一个橙色的橘子。",
|
||
]
|
||
first_qtypes = [(True, None)] * 5
|
||
first_questions = build_qs(qsid, first_words, first_descs, first_qtypes, 0)
|
||
|
||
second_words = ["bag", "dress", "jacket", "hat", "T-shirt"]
|
||
second_descs = [
|
||
"白色的背景,中间是一个书包。",
|
||
"白色的背景,中间是一条连衣裙。",
|
||
"白色的背景,中间是一件夹克衫。",
|
||
"白色的背景,中间是一顶帽子。",
|
||
"白色的背景,中间是一件T恤衫。",
|
||
]
|
||
second_qtypes = [(False, None)] * 5
|
||
second_questions = build_qs(qsid, second_words, second_descs, second_qtypes, 5)
|
||
|
||
first_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(first_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in first_questions],
|
||
}
|
||
second_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(second_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in second_questions],
|
||
}
|
||
|
||
json_data = json.dumps({"first": first_qs_obj, "second": second_qs_obj}, ensure_ascii=False)
|
||
config1 = build_human_config(qsid, ", ".join(first_words), first_questions)
|
||
config2 = build_human_config(qsid, ", ".join(second_words), second_questions)
|
||
|
||
fields = {
|
||
"jsonData": json_data,
|
||
"题目1 完整配置": config1,
|
||
"题目2 完整配置": config2,
|
||
"审校结果": "✅ OK | 2026-05-18 小研审校",
|
||
}
|
||
resp = update_record("recvjYhcXkYXIM", fields)
|
||
print(f"100001: code={resp.get('code')} | first={len(first_words)} second={len(second_words)}")
|
||
|
||
# ============================================================
|
||
# ID: 110101 — first=6→5 (drop monster), second=6→5 (drop duplicate colour)
|
||
# ============================================================
|
||
qsid = "110101"
|
||
first_words = ["hair", "eye", "nose", "foot", "hand"]
|
||
first_descs = [
|
||
"白色的背景,中间是一个人的头发。",
|
||
"白色的背景,中间是一只眼睛。",
|
||
"白色的背景,中间是一个鼻子。",
|
||
"白色的背景,中间是一只脚。",
|
||
"白色的背景,中间是一只手。",
|
||
]
|
||
first_qtypes = [(False, None)] * 5
|
||
first_questions = build_qs(qsid, first_words, first_descs, first_qtypes, 0)
|
||
|
||
second_words = ["black", "brown", "colour", "white", "yellow"]
|
||
second_descs = [
|
||
"白色的背景,中间是一只黑色的猫。",
|
||
"白色的背景,中间是一只棕色的小狗。",
|
||
"白色的背景,中间是一个彩色的调色盘。",
|
||
"白色的背景,中间是一只白色的兔子。",
|
||
"白色的背景,中间是一朵黄色的向日葵。",
|
||
]
|
||
# colour is not a color answer → use "What's this?"
|
||
second_qtypes = [
|
||
(True, None), # black
|
||
(True, None), # brown
|
||
(False, "What's this?"), # colour — 颜色概念
|
||
(True, None), # white
|
||
(True, None), # yellow
|
||
]
|
||
second_questions = build_qs(qsid, second_words, second_descs, second_qtypes, 5)
|
||
|
||
first_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(first_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in first_questions],
|
||
}
|
||
second_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(second_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in second_questions],
|
||
}
|
||
|
||
json_data = json.dumps({"first": first_qs_obj, "second": second_qs_obj}, ensure_ascii=False)
|
||
config1 = build_human_config(qsid, ", ".join(first_words), first_questions)
|
||
config2 = build_human_config(qsid, ", ".join(second_words), second_questions)
|
||
|
||
fields = {
|
||
"jsonData": json_data,
|
||
"题目1 完整配置": config1,
|
||
"题目2 完整配置": config2,
|
||
"审校结果": "✅ OK | 2026-05-18 小研审校",
|
||
}
|
||
resp = update_record("recvjYhdvUxDgs", fields)
|
||
print(f"110101: code={resp.get('code')} | first={len(first_words)} second={len(second_words)}")
|
||
|
||
# ============================================================
|
||
# ID: 110201 — first=6→5 (move ice cream to second), second=4→5
|
||
# ============================================================
|
||
qsid = "110201"
|
||
first_words = ["bread", "pie", "cake", "candy", "chocolate"]
|
||
first_descs = [
|
||
"白色的背景,中间是一片面包。",
|
||
"白色的背景,中间是一块馅饼。",
|
||
"白色的背景,中间是一块蛋糕。",
|
||
"白色的背景,中间是一颗糖果。",
|
||
"白色的背景,中间是一块巧克力。",
|
||
]
|
||
first_qtypes = [(False, None)] * 5
|
||
first_questions = build_qs(qsid, first_words, first_descs, first_qtypes, 0)
|
||
|
||
second_words = ["cat", "dog", "mice", "mouse", "ice cream"]
|
||
second_descs = [
|
||
"白色的背景,中间是一只猫。",
|
||
"白色的背景,中间是一只狗。",
|
||
"白色的背景,中间是两只老鼠。",
|
||
"白色的背景,中间是一只老鼠。",
|
||
"白色的背景,中间是一个冰淇淋。",
|
||
]
|
||
second_qtypes = [
|
||
(False, None), # cat
|
||
(False, None), # dog
|
||
(False, "What are these?"), # mice (plural)
|
||
(False, None), # mouse
|
||
(False, None), # ice cream
|
||
]
|
||
second_questions = build_qs(qsid, second_words, second_descs, second_qtypes, 5)
|
||
|
||
first_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(first_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in first_questions],
|
||
}
|
||
second_qs_obj = {
|
||
"category": "speaking", "type": "speaking_pic_recognize",
|
||
"asrPrompt": ", ".join(second_words), "questionSetID": qsid,
|
||
"textTitle": "Look and answer.",
|
||
"questionSet": [{k: v for k, v in q.items() if k != "shortExplanation"} for q in second_questions],
|
||
}
|
||
|
||
json_data = json.dumps({"first": first_qs_obj, "second": second_qs_obj}, ensure_ascii=False)
|
||
config1 = build_human_config(qsid, ", ".join(first_words), first_questions)
|
||
config2 = build_human_config(qsid, ", ".join(second_words), second_questions)
|
||
|
||
fields = {
|
||
"jsonData": json_data,
|
||
"题目1 完整配置": config1,
|
||
"题目2 完整配置": config2,
|
||
"审校结果": "✅ OK | 2026-05-18 小研审校",
|
||
}
|
||
resp = update_record("recvjYhe4opOGm", fields)
|
||
print(f"110201: code={resp.get('code')} | first={len(first_words)} second={len(second_words)}")
|
||
|
||
# ============================================================
|
||
# VERIFY ALL
|
||
# ============================================================
|
||
print("\n=== VERIFICATION ===")
|
||
for qsid, rid in [("100001","recvjYhcXkYXIM"),("110101","recvjYhdvUxDgs"),("110201","recvjYhe4opOGm")]:
|
||
r = subprocess.run([
|
||
'curl', '-s', '-X', 'GET',
|
||
f'https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{rid}',
|
||
'-H', f'Authorization: Bearer {TOKEN}'
|
||
], capture_output=True, text=True)
|
||
f = json.loads(r.stdout)['data']['record']['fields']
|
||
jd = json.loads(f['jsonData'])
|
||
f_count = len(jd['first']['questionSet'])
|
||
s_count = len(jd['second']['questionSet'])
|
||
review = f.get('审校结果', 'N/A')
|
||
|
||
issues = []
|
||
if f_count != 5: issues.append(f"first={f_count}≠5")
|
||
if s_count != 5: issues.append(f"second={s_count}≠5")
|
||
if not review: issues.append("missing review")
|
||
|
||
# Spot check: explanations are Chinese
|
||
for sec_name, sec in [("first", jd['first']), ("second", jd['second'])]:
|
||
for qi, q in enumerate(sec['questionSet']):
|
||
exp = q.get('explanation','')
|
||
if not any('\u4e00' <= c <= '\u9fff' for c in exp):
|
||
issues.append(f"{sec_name}Q{qi} explanation not Chinese")
|
||
|
||
status = '✅' if not issues else '❌'
|
||
print(f"{status} {qsid}: first={f_count} second={s_count} review={review}")
|
||
if issues:
|
||
for iss in issues:
|
||
print(f" ❌ {iss}")
|
||
|
||
print("\n✅ VERIFICATION COMPLETE")
|