#!/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")