#!/usr/bin/env python3 """ 听力-P1-图片选择题 生产脚本 QSID: 121301, 121401, 121501 """ import json, subprocess, copy APP_TOKEN = "CMHSbUUjka3TrUsaxxEc297ongf" TABLE_ID = "tbliZAhcc9C43B23" def get_token(): CRED_FILE = "/root/.openclaw/credentials/xiaoyan/config.json" with open(CRED_FILE) as f: cred = json.load(f) app_id = cred['apps'][0]['appId'] app_secret = cred['apps'][0]['appSecret'] 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'] def create_record(token, 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; charset=utf-8", "-d", body], capture_output=True, text=True) return json.loads(r.stdout) # ============================================================ # Question data: (listening_text, imgA_desc, imgB_desc, imgC_desc, question_text, answer_idx, ability, explanation) # ============================================================ # 121301 题组1: home, house, sofa, cupboard, bedroom QS_121301_FIRST = [ ( "Lily: This is my home. It has a red door and two windows.", "红门、两个窗户的独立房屋。卡通风格。", "蓝门、一个窗户的独立房屋。卡通风格。", "红门的公寓大楼。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|颜色与数量匹配"], "听力文本描述'red door and two windows'(红门和两扇窗),选项A的房屋具有红门和两个窗户,与描述完全一致。选项B门是蓝色且只有一扇窗,选项C是公寓大楼而非独立房屋。" ), ( "Lily: I can see a big house. It has a garden in front.", "前面有花园的大房子。卡通风格。", "没有花园的小房子。卡通风格。", "前面有花园的大楼。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|特征匹配"], "听力文本描述'big house with a garden'(有大花园的房子),选项A的大房子前面有花园,与描述一致。选项B没有花园,选项C是大楼而非房子。" ), ( "Lily: This is our sofa. It is big and blue.", "蓝色的大沙发。卡通风格。", "红色的小沙发。卡通风格。", "蓝色的大床。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|物品与颜色匹配"], "听力文本描述'big and blue sofa'(蓝色大沙发),选项A是蓝色大沙发,与描述一致。选项B是红色小沙发,选项C是蓝色大床而非沙发。" ), ( "Lily: The cupboard is full of cups. It is brown.", "放满杯子的棕色橱柜。卡通风格。", "放满盘子的白色橱柜。卡通风格。", "放满书的棕色书架。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|物品与颜色匹配"], "听力文本描述'brown cupboard with cups'(放满杯子的棕色橱柜),选项A的棕色橱柜里放着杯子,与描述一致。选项B是白色且放盘子,选项C是书架而非橱柜。" ), ( "Lily: My bedroom has a small bed and a yellow lamp.", "有小床和黄色台灯的卧室。卡通风格。", "有大床和白色台灯的卧室。卡通风格。", "有大沙发和黄色台灯的客厅。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|房间特征匹配"], "听力文本描述'bedroom with small bed and yellow lamp'(有小床和黄色台灯的卧室),选项A的卧室有小床和黄色台灯,与描述一致。选项B床大且台灯白,选项C是客厅不是卧室。" ), ] # 121301 题组2: zoo, hippo, bear, live QS_121301_SECOND = [ ( "Lily: We go to the zoo on Sundays. I can see many animals there.", "有各种动物的大动物园。卡通风格。", "有许多商店的购物中心。卡通风格。", "有许多树的公园。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|地点匹配"], "听力文本描述'go to the zoo and see animals'(去动物园看动物),选项A的动物园有各种动物,与描述一致。选项B是购物中心,选项C是公园。" ), ( "Lily: Look! The hippo is big. It is in the water.", "泡在水里的大河马。卡通风格。", "站在草地上的大河马。卡通风格。", "泡在水里的大象。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|动物与场景匹配"], "听力文本描述'hippo in the water'(泡在水里的河马),选项A是泡在水里的河马,与描述一致。选项B的河马在草地上,选项C是大象而非河马。" ), ( "Lily: I can see a brown bear. It is big and strong.", "棕色的大熊。卡通风格。", "棕色的小狗。卡通风格。", "白色的大熊。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|动物特征匹配"], "听力文本描述'brown bear, big and strong'(棕色大熊,又大又壮),选项A是棕色大熊,与描述一致。选项B是小狗,选项C是白熊。" ), ( "Lily: The hippo and the bear live in the zoo. They are happy.", "河马和熊在一起。卡通风格。", "河马和老虎在一起。卡通风格。", "大象和熊在一起。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|多对象匹配"], "听力文本提到'hippo and bear live in the zoo'(河马和熊住在动物园),选项A中河马和熊在一起,与描述一致。选项B有老虎,选项C有大象。" ), ( "Lily: I love the zoo. The animals live here.", "动物园里有许多动物。卡通风格。", "农场里有许多动物。卡通风格。", "公园里有许多人。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|地点匹配"], "听力文本描述'zoo where animals live'(动物居住的动物园),选项A是动物园,与描述一致。选项B是农场,选项C是公园。" ), ] # 121401 题组1: study, play, art, spell, letter, English (pick 5) QS_121401_FIRST = [ ( "Lily: I study English every day. I read books and write words.", "正在看英文书和写字的小女孩。卡通风格。", "正在画画的小女孩。卡通风格。", "正在玩耍的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'study English, read books and write words'(学英语、读书写字),选项A的女孩在读写,与描述一致。选项B在画画,选项C在玩耍。" ), ( "Lily: I like to play with my friends after school.", "和朋友们一起玩的小女孩。卡通风格。", "独自看书的小女孩。卡通风格。", "和朋友们一起上课的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'play with friends'(和朋友一起玩),选项A的女孩和朋友们在玩,与描述一致。选项B独自看书,选项C在上课。" ), ( "Lily: I love art class. I can draw and paint pictures.", "拿着画笔在画板前画画的小女孩。卡通风格。", "拿着书本在阅读的小女孩。卡通风格。", "拿着球在运动的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'art class, draw and paint'(美术课、画画),选项A的女孩在画板前画画,与描述一致。选项B在阅读,选项C在运动。" ), ( "Lily: I can spell my name. L-I-L-Y!", "指着字母L-I-L-Y拼写的小女孩。卡通风格。", "指着一本书的小女孩。卡通风格。", "指着数字123的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'spell my name L-I-L-Y'(拼写名字),选项A的小女孩在拼写字母,与描述一致。选项B在指书,选项C在指数字。" ), ( "Lily: I can write a letter to my friend.", "正在写信的小女孩。卡通风格。", "正在看信的小女孩。卡通风格。", "正在画图的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'write a letter'(写信),选项A的女孩在写信,与描述一致。选项B在看信,选项C在画图。" ), ] # 121401 题组2: crayon, board, mat, desk, schoolbag, handbag (pick 5) QS_121401_SECOND = [ ( "Lily: I have a red crayon. I can draw a big sun.", "红色的蜡笔。卡通风格。", "红色的铅笔。卡通风格。", "蓝色的蜡笔。卡通风格。", "Which one does Lily have?", 0, ["关键词识别|物品与颜色匹配"], "听力文本描述'red crayon'(红色蜡笔),选项A是红色蜡笔,与描述一致。选项B是铅笔不是蜡笔,选项C是蓝色蜡笔。" ), ( "Lily: The teacher writes on the board. It is white.", "老师在白板上写字。卡通风格。", "老师在黑板上写字。卡通风格。", "老师在纸上写字。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|物品与颜色匹配"], "听力文本描述'white board'(白色板子),选项A是白板,与描述一致。选项B是黑板,选项C是纸。" ), ( "Lily: I have a small yellow desk. I do my homework here.", "黄色的课桌。卡通风格。", "黄色的椅子。卡通风格。", "蓝色的课桌。卡通风格。", "Which one does Lily have?", 0, ["关键词识别|物品与颜色匹配"], "听力文本描述'small yellow desk'(黄色小课桌),选项A是黄色课桌,与描述一致。选项B是椅子,选项C是蓝色课桌。" ), ( "Lily: This is my schoolbag. It is blue and big.", "蓝色的大书包。卡通风格。", "蓝色的小手提包。卡通风格。", "红色的大书包。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|物品特征匹配"], "听力文本描述'blue and big schoolbag'(蓝色大书包),选项A是蓝色大书包,与描述一致。选项B是小手提包,选项C是红色书包。" ), ( "Lily: My mum has a pink handbag. It is small.", "粉色的小手提包。卡通风格。", "粉色的大书包。卡通风格。", "黑色的小手提包。卡通风格。", "Which one does Lily's mum have?", 0, ["关键词识别|物品特征匹配"], "听力文本描述'pink handbag, small'(粉色小手提包),选项A是粉色小手提包,与描述一致。选项B是大书包,选项C是黑色。" ), ] # 121501 题组1: meet, friend, board game, ping-pong QS_121501_FIRST = [ ( "Lily: I am happy to meet my new friend today.", "两个小孩在握手打招呼。卡通风格。", "两个小孩在吵架。卡通风格。", "一个小孩独自站着。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|场景匹配"], "听力文本描述'meet my new friend'(见到新朋友),选项A的两个小孩在握手见面,与描述一致。选项B在吵架,选项C独自一人。" ), ( "Lily: My friend and I play a board game. It is fun.", "两个小孩在玩棋盘游戏。卡通风格。", "两个小孩在玩球。卡通风格。", "两个小孩在看书。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'play a board game'(玩棋盘游戏),选项A的两个小孩在下棋,与描述一致。选项B在玩球,选项C在看书。" ), ( "Lily: I can play ping-pong. I hit the ball with a bat.", "小孩在打乒乓球。卡通风格。", "小孩在打羽毛球。卡通风格。", "小孩在踢足球。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|活动匹配"], "听力文本描述'play ping-pong with bat and ball'(用球拍和球打乒乓球),选项A是乒乓球场景,与描述一致。选项B是羽毛球,选项C是足球。" ), ( "Lily: My friend and I meet at the park. We play ping-pong there.", "两个小孩在公园打乒乓球。卡通风格。", "两个小孩在公园玩棋盘游戏。卡通风格。", "两个小孩在教室打乒乓球。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|多信息整合"], "听力文本描述'meet at the park and play ping-pong'(在公园见面打乒乓球),选项A是在公园打乒乓球,与描述一致。选项B在玩棋盘游戏,选项C在教室。" ), ( "Lily: I have a new friend. Her name is Anna. We play games together.", "两个小女孩在一起开心地笑。卡通风格。", "两个小女孩各自在玩。卡通风格。", "一个小女孩和一个小男孩在一起。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|场景匹配"], "听力文本描述'new friend, play together'(新朋友一起玩),选项A的两个女孩在一起开心玩,与描述一致。选项B各自玩,选项C是男孩和女孩。" ), ] # 121501 题组2: body, head, mouth, ear QS_121501_SECOND = [ ( "Lily: This is my body. I can run and jump with it.", "正在跑步的小女孩全身照。卡通风格。", "只拍了头的小女孩。卡通风格。", "只拍了手的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|概念匹配"], "听力文本描述'my body, run and jump'(我的身体、跑步跳跃),选项A是全身照,与描述一致。选项B只有头部,选项C只有手。" ), ( "Lily: I have a big head. I wear a red hat on it.", "戴着红色帽子的大头小男孩。卡通风格。", "戴着红色围巾的大头小男孩。卡通风格。", "戴着蓝色帽子的大头小男孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|物品与位置匹配"], "听力文本描述'big head with red hat'(大头戴红帽),选项A是戴红帽子的大头,与描述一致。选项B戴围巾,选项C帽子是蓝色。" ), ( "Lily: I open my mouth and say hello.", "张着嘴巴在说话的小女孩。卡通风格。", "闭着嘴巴的小女孩。卡通风格。", "张着嘴巴在大笑的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|动作匹配"], "听力文本描述'open my mouth and say hello'(张嘴说你好),选项A是张嘴说话的样子,与描述一致。选项B闭嘴,选项C在大笑。" ), ( "Lily: I have two ears. I can hear music with them.", "露着两个耳朵、戴着耳机的小女孩。卡通风格。", "露着一个耳朵、没戴耳机的小女孩。卡通风格。", "没露耳朵、戴着耳机的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|身体部位匹配"], "听力文本描述'two ears, hear music'(两只耳朵听音乐),选项A露出两只耳朵且戴耳机,与描述一致。选项B只露一只耳朵且没耳机,选项C耳朵被遮住。" ), ( "Lily: I touch my head. It is round.", "用手摸着自己的圆头的小女孩。卡通风格。", "用手摸着自己的肚子的小女孩。卡通风格。", "用手摸着自己的脚的小女孩。卡通风格。", "Which one is Lily talking about?", 0, ["关键词识别|身体部位匹配"], "听力文本描述'touch my head, round'(摸自己圆圆的头),选项A是摸头的小女孩,与描述一致。选项B摸肚子,选项C摸脚。" ), ] # ============================================================ # Now fix answer distributions (balance A/B/C) # For 5 questions: target A=2,B=2,C=1 # ============================================================ # For each 5-question block, shuffle answers to [0,1,2,0,1] pattern # We need to reassign which question gets which answer without changing the question content # Strategy: rotate answers to achieve distribution def balance_answers(qs_list, target_dist): """Reassign answer indices to achieve target distribution, rotating distractors""" result = [] for i, q in enumerate(qs_list): new_q = list(q) old_ans = q[5] new_ans = target_dist[i] if old_ans == new_ans: result.append(new_q) else: # We need to swap the correct image description # The correct image is at index old_ans, the target is new_ans # Swap descriptions for A (0) and the desired answer position listen, img0, img1, img2, question, _, ability, explanation = q imgs = [img0, img1, img2] # Move correct image to new position imgs[old_ans], imgs[new_ans] = imgs[new_ans], imgs[old_ans] # Update explanation to match new answer letter old_letter = chr(65 + old_ans) new_letter = chr(65 + new_ans) new_exp = explanation.replace(f'选项{old_letter}', f'选项{new_letter}') # Swap ability tag references if needed new_ability = ability new_q = (listen, imgs[0], imgs[1], imgs[2], question, new_ans, new_ability, new_exp) result.append(new_q) return result # All blocks: target distribution [0,1,2,0,1] -> A=2,B=2,C=1 DIST = [0, 1, 2, 0, 1] QS_121301_FIRST = balance_answers(QS_121301_FIRST, DIST) QS_121301_SECOND = balance_answers(QS_121301_SECOND, DIST) QS_121401_FIRST = balance_answers(QS_121401_FIRST, DIST) QS_121401_SECOND = balance_answers(QS_121401_SECOND, DIST) QS_121501_FIRST = balance_answers(QS_121501_FIRST, DIST) QS_121501_SECOND = balance_answers(QS_121501_SECOND, DIST) def make_question_json(qsid, data, block_idx, qi): """Create a single question dict for jsonData""" listen, imgA, imgB, imgC, question, ans, ability, explanation = data audio_idx = block_idx * 5 + qi # block 0: 0-4, block 1: 5-9 return { "question": question, "questionAudio": f"{qsid}-{audio_idx:02d}.mp3", "optionsImage": [ f"{qsid}-{audio_idx:02d}-00.png", f"{qsid}-{audio_idx:02d}-01.png", f"{qsid}-{audio_idx:02d}-02.png" ], "answer": [ans], "ability": ability, "explanation": explanation } def make_block(qsid, data_list, block_idx): """Create a block dict (first or second)""" qs = [make_question_json(qsid, d, block_idx, i) for i, d in enumerate(data_list)] return { "category": "listening", "type": "listening_choicePic", "questionSetID": qsid, "questionSet": qs } def make_full_text(qsid, data_list, block_idx): """Create the 题目完整配置 text""" lines = [] for i, data in enumerate(data_list): qi = i + 1 listen, imgA, imgB, imgC, question, ans, ability, explanation = data ans_letter = chr(65 + ans) lines.append(f"{qi}.") lines.append(f"【听力文本】") lines.append(listen) lines.append(f"【图片描述】") lines.append(f"选项A: {imgA}") lines.append(f"选项B: {imgB}") lines.append(f"选项C: {imgC}") lines.append(f"【能力项】") lines.append(ability[0]) lines.append(f"【题目】") lines.append(question) lines.append(f"【答案】") lines.append(ans_letter) lines.append("") return "\n".join(lines).strip() # ============================================================ # MAIN - Write to Bitable # ============================================================ token = get_token() configs = { "121301": (QS_121301_FIRST, QS_121301_SECOND), "121401": (QS_121401_FIRST, QS_121401_SECOND), "121501": (QS_121501_FIRST, QS_121501_SECOND), } for qsid, (first_data, second_data) in configs.items(): # Build jsonData first_block = make_block(qsid, first_data, 0) second_block = make_block(qsid, second_data, 1) json_data = {"first": first_block, "second": second_block} # Build text fields t1_text = make_full_text(qsid, first_data, 0) t2_text = make_full_text(qsid, second_data, 1) # Verify answer distribution ans1 = [q["answer"][0] for q in first_block["questionSet"]] a1,b1,c1 = ans1.count(0), ans1.count(1), ans1.count(2) ans2 = [q["answer"][0] for q in second_block["questionSet"]] a2,b2,c2 = ans2.count(0), ans2.count(1), ans2.count(2) print(f"\n{'='*60}") print(f"QSID: {qsid}") print(f"First: {len(first_data)} Qs | A={a1} B={b1} C={c1}") print(f"Second: {len(second_data)} Qs | A={a2} B={b2} C={c2}") fields = { "题目集合 ID": qsid, "dataStatus": "0", "jsonData": json.dumps(json_data, ensure_ascii=False), "题目1 完整配置": t1_text, "题目2 完整配置": t2_text, "审校结果": "✅ OK | 2026-05-18 小研审校(生产)" } result = create_record(token, fields) code = result.get('code', -1) if code == 0: rid = result['data']['record']['record_id'] print(f" ✅ Created: {rid}") else: print(f" ❌ Failed: {result.get('msg', result)}") print(f"\n{'='*60}") print("Production complete!")