#!/usr/bin/env python3 import argparse import sys import os import yaml from openai import OpenAI from pathlib import Path # 加载配置 BASE_DIR = Path(__file__).parent.parent ASSETS_DIR = BASE_DIR / "assets" # 加载外部配置文件(所有可调优参数全部在assets目录下的yaml文件,无需改代码) try: # 基础配置 with open(ASSETS_DIR / "sci_fi_map.yaml", "r", encoding="utf-8") as f: SCI_FI_WORD_MAP = yaml.safe_load(f) with open(ASSETS_DIR / "stage_config.yaml", "r", encoding="utf-8") as f: STAGE_CONFIG = yaml.safe_load(f) # 调优配置 with open(ASSETS_DIR / "expression_map.yaml", "r", encoding="utf-8") as f: EXPRESSION_MAP = yaml.safe_load(f) with open(ASSETS_DIR / "prompt_config.yaml", "r", encoding="utf-8") as f: PROMPT_CONFIG = yaml.safe_load(f) with open(ASSETS_DIR / "validation_config.yaml", "r", encoding="utf-8") as f: VALIDATION_CONFIG = yaml.safe_load(f) # 词表配置 with open(BASE_DIR / "references" / "l1_word_list.json", "r", encoding="utf-8") as f: L1_WORD_LIST = set([word.lower() for word in yaml.safe_load(f)]) except Exception as e: print(f"❌ 配置文件加载失败,请检查yaml格式是否正确: {str(e)}") sys.exit(1) # 初始化LLM客户端,配置从环境变量读取 try: client = OpenAI( api_key=os.getenv("OPENAI_API_KEY", "your-api-key"), base_url=os.getenv("OPENAI_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3") ) MODEL = os.getenv("OPENAI_MODEL", "volcengine/doubao-seed-2-0-pro-260215") except Exception as e: print(f"❌ LLM客户端初始化失败: {str(e)}") sys.exit(1) def load_input(input_path): """加载输入内容,支持单个文件或目录批量加载""" input_path = Path(input_path) if not input_path.exists(): print(f"❌ 输入路径不存在: {input_path}") sys.exit(1) if input_path.is_file(): with open(input_path, "r", encoding="utf-8") as f: return [(input_path.name, f.read())] elif input_path.is_dir(): # 批量加载目录下所有txt文件 script_files = list(input_path.glob("*.txt")) if not script_files: print(f"❌ 目录下没有找到txt格式的剧本文件: {input_path}") sys.exit(1) results = [] for f in script_files: with open(f, "r", encoding="utf-8") as fp: results.append((f.name, fp.read())) return results else: print(f"❌ 不支持的输入类型: {input_path}") sys.exit(1) def get_prompt(input_text, stage): """生成Prompt,所有可调规则从配置文件读取,无需改代码""" sci_fi_map_str = "\n".join([f"{k} → {v}" for k, v in SCI_FI_WORD_MAP.items()]) # 动态加载配置规则 emotion_map_rule = "优先使用以下映射匹配情绪词:" + "、".join([f"{k}→{v}" for k,v in EXPRESSION_MAP['emotion_map'].items()]) if PROMPT_CONFIG['naturalization']['enable_emotion_word'] else "不使用自定义情绪词映射" synonym_replace_rule = "可使用以下同义口语替换(不改变原意):" + "、".join([f"{k}→{v}" for k,v in EXPRESSION_MAP['synonym_replace'].items()]) if PROMPT_CONFIG['naturalization']['enable_synonym_replace'] else "不使用同义替换" split_rule = "包含2个及以上信息的句子拆成单信息短句" if PROMPT_CONFIG['naturalization']['enable_long_sentence_split'] else "不拆分长句" repeat_rule = "允许自然重复(比如It is dirty. Very dirty.)" if PROMPT_CONFIG['naturalization']['allow_repeat_expression'] else "不允许重复表达" exclamation_rule = "情绪强烈的句子可用感叹号" if PROMPT_CONFIG['naturalization']['enable_exclamation_mark'] else "统一使用句号" fidelity_rule = "100%忠于原剧本内容:禁止新增任何原剧本没有的信息、禁止删除任何原剧本已有的信息" if PROMPT_CONFIG['script_fidelity']['strictly_no_add'] and PROMPT_CONFIG['script_fidelity']['strictly_no_delete'] else "允许适当调整细节" return f""" 你是专为4-8岁儿童打造的英文台词生产专家,严格遵守以下所有规则生成内容,绝对不允许违反: ### 剧本忠实度规则(最高优先级,绝对不能违反) {fidelity_rule} ### 第一步:输入归一 当前输入是:{input_text} 不管输入是纯中文/纯英文/中英混合,你首先统一转成标准中文「角色: 台词」格式,完整保留所有剧情、动作、角色关系、道具、事件触发点信息,不能丢失任何核心内容。 ### 第二步:中文AR预处理 严格遵守4个保留机制(绝对不能改): 1. 保留完整事件动词链 2. 保留所有事件触发点 3. 保留完整道具逻辑链 4. 保留原有角色关系 按以下7条规则拆成单信息短句,1句仅表达1个信息,不改变剧情: 1. 复杂句拆成短句 2. 因果拆分,保留事实不保留连接词 3. 目的拆分,不删目的信息 4. 多步动作拆成单动作句 5. 条件+行为全拆分,去掉假设逻辑 6. 情绪与事实拆分,不修改情绪 7. 去复杂推理,只留可见事实 ### 第三步:分级英文生成 目标Stage:{stage} 对应要求:{STAGE_CONFIG[stage]["rules"]} 蓝思值要求:{STAGE_CONFIG[stage]["lexile"]} 自然化要求(**严格遵守剧本忠实度规则,禁止新增/删减任何原剧本没有的内容**): 1. 情绪词映射规则:{emotion_map_rule} 2. 同义替换规则:{synonym_replace_rule} 3. 长句拆分规则:{split_rule} 4. 重复表达规则:{repeat_rule} 5. 标点规则:{exclamation_rule} 6. 绝对禁止成人化连接词(actually/in fact/however等) 7. 完全符合母语小朋友说话习惯,绝对不能有翻译腔 8. 科幻词汇自动按以下映射替换: {sci_fi_map_str} ### 第四步:自动校验 生成后自行校验以下4项: 1. AR等级合规:S1禁止AR3/AR4,S2禁止AR4 2. 难度合规:词汇/句法/句长/蓝思值完全匹配对应Stage要求,无超纲 3. 自然度合规:无翻译腔,符合4-8岁儿童母语表达习惯 4. 内容合规:无敏感内容,无中式英语 ### 输出格式(严格按照格式输出,不要其他内容) 【Stage {stage} 英文台词(适配{STAGE_CONFIG[stage]["age"]})】 角色A: 台词内容 角色B: 台词内容 ... 【蓝思值】:[估算值]L 【校验结果】:通过/待优化 【优化建议】:无/具体建议 """ def generate_single_script(input_text, stage): """生成单个剧本的台词""" try: prompt = get_prompt(input_text, stage) response = client.chat.completions.create( model=MODEL, messages=[{"role": "user", "content": prompt}], temperature=0.3, max_tokens=2000, timeout=30 ) result = response.choices[0].message.content # 增加超纲词校验 oov_words = check_out_of_vocab(result, stage) if oov_words and stage in ["S1", "S2"]: result += f"\n【超纲词提醒】:{', '.join(oov_words)}(请确认是否需要替换)" return result except Exception as e: return f"❌ 生成失败: {str(e)}" def check_out_of_vocab(script_content, stage): """检查超纲词汇,规则从配置文件读取""" if not VALIDATION_CONFIG['vocab_validation']['enable_OOV_remind'] or stage not in ["S1", "S2"]: return [] # 提取所有英文单词 import re words = re.findall(r"[a-zA-Z']+", script_content) words = [word.lower().strip("'") for word in words] # 过滤配置里定义的停用词 stop_words = set(VALIDATION_CONFIG['vocab_validation']['stop_words']) words = [word for word in words if word not in stop_words and len(word) > 1] # 找超纲词 out_of_vocab = list(set([word for word in words if word not in L1_WORD_LIST])) return out_of_vocab def save_result(output_dir, filename, content): """保存结果到文件""" output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) output_file = output_dir / f"result_{filename}" with open(output_file, "w", encoding="utf-8") as f: f.write(content) return output_file def main(): parser = argparse.ArgumentParser(description="4-8岁儿童英文台词标准化生产工具") group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--input", type=str, help="直接输入待处理的剧本文本") group.add_argument("--path", type=str, help="待处理的单个剧本文件路径或包含多个剧本的目录路径") parser.add_argument("--stage", type=str, choices=["S1", "S2", "S3", "S4"], required=True, help="目标难度等级 S1/S2/S3/S4") parser.add_argument("--output", type=str, help="结果输出目录,不指定则直接打印到控制台") args = parser.parse_args() # 处理输入 if args.input: input_list = [("direct_input", args.input)] else: input_list = load_input(args.path) # 批量生成 results = [] for filename, text in input_list: print(f"\n🚀 正在处理: {filename}") result = generate_single_script(text, args.stage) results.append((filename, result)) print(result) # 保存结果 if args.output: save_path = save_result(args.output, filename, result) print(f"💾 结果已保存到: {save_path}") print(f"\n✅ 全部处理完成,共处理{len(results)}个剧本") if __name__ == "__main__": main()