| """ | |
| 交互式翻译推理脚本 | |
| 使用方式: | |
| # 命令行交互翻译 | |
| python scripts/translate.py --checkpoint checkpoints/best_model.pt | |
| # 翻译文件 | |
| python scripts/translate.py --checkpoint checkpoints/best_model.pt --input input.txt --output output.txt | |
| # 启动 Streamlit Web UI(无需模型即可预览界面) | |
| python scripts/translate.py --web | |
| # 或者带模型启动翻译 | |
| python scripts/translate.py --checkpoint checkpoints/best_model.pt --web | |
| """ | |
| import argparse | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| try: | |
| from omegaconf import OmegaConf | |
| except ImportError: # pragma: no cover | |
| OmegaConf = None | |
| import yaml | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) | |
| import torch | |
| from easytranslate.data.collator import TranslationCollator | |
| from easytranslate.data.dataset import ( | |
| TranslationDataset, | |
| load_custom_dataset, | |
| load_opus_dataset, | |
| load_wmt_dataset, | |
| ) | |
| from easytranslate.data.tokenizer import TokenizerWrapper, build_tokenizer | |
| from easytranslate.evaluation.evaluator import Evaluator | |
| from easytranslate.model import TransformerTranslationModel | |
| from easytranslate.model.finetune import load_pretrained_model | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description="EasyTranslate Inference") | |
| parser.add_argument("--config", type=str, default="configs/default_config.yaml") | |
| parser.add_argument("--checkpoint", type=str, default=None) | |
| parser.add_argument("--input", type=str, default=None, help="输入文件路径") | |
| parser.add_argument("--output", type=str, default=None, help="输出文件路径") | |
| parser.add_argument("--web", action="store_true", help="启动 Streamlit Web UI") | |
| parser.add_argument("--streamlit-app", action="store_true", help=argparse.SUPPRESS) | |
| args, unknown = parser.parse_known_args() | |
| return args, unknown | |
| def _get_config(config, *keys, default=None): | |
| value = config | |
| for key in keys: | |
| if isinstance(value, dict): | |
| value = value.get(key, default) | |
| else: | |
| value = getattr(value, key, default) | |
| if value is default: | |
| break | |
| return value | |
| def _load_config(path, cli_overrides=None): | |
| if OmegaConf is not None: | |
| config = OmegaConf.load(path) | |
| if cli_overrides: | |
| config = OmegaConf.merge(config, OmegaConf.from_cli(cli_overrides)) | |
| return config | |
| with open(path, "r", encoding="utf-8") as fin: | |
| config = yaml.safe_load(fin) | |
| if cli_overrides: | |
| print("Warning: OmegaConf is not installed; CLI overrides are ignored.") | |
| return config | |
| def interactive_translate(evaluator): | |
| print("输入英⽂句⼦,按回车翻译;输入 'quit' 退出。") | |
| while True: | |
| try: | |
| text = input("> ").strip() | |
| except EOFError: | |
| break | |
| if not text: | |
| continue | |
| if text.lower() in {"quit", "exit"}: | |
| break | |
| translation = evaluator.translate_single(text) | |
| print(translation) | |
| def translate_file(evaluator, input_path: str, output_path: str): | |
| source_lines = [] | |
| with Path(input_path).open("r", encoding="utf-8") as fin: | |
| for line in fin: | |
| line = line.strip() | |
| if line: | |
| source_lines.append(line) | |
| translations = evaluator.translate(source_lines) | |
| output_file = Path(output_path) | |
| output_file.parent.mkdir(parents=True, exist_ok=True) | |
| with output_file.open("w", encoding="utf-8") as fout: | |
| for line in translations: | |
| fout.write(f"{line}\n") | |
| print(f"Translation complete: {len(translations)} lines written to {output_path}") | |
| def launch_streamlit_app(args): | |
| import streamlit as st | |
| def load_evaluator(): | |
| config = _load_config(args.config) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model, tokenizer = build_model_and_tokenizer(config, device) | |
| model = load_checkpoint(model, args.checkpoint, device) | |
| return Evaluator(model, tokenizer, config) | |
| st.set_page_config(page_title="EasyTranslate", layout="wide") | |
| st.title("EasyTranslate") | |
| st.write("English to Chinese translation powered by EasyTranslate.") | |
| if args.checkpoint is None: | |
| st.warning( | |
| "当前未提供模型 checkpoint,页面仅用于预览界面效果。" | |
| " 如需翻译,请传入 --checkpoint 或先训练生成模型。" | |
| ) | |
| input_text = st.text_area("English Input", value="", height=200) | |
| if st.button("Translate"): | |
| if not input_text.strip(): | |
| st.warning("请输入要翻译的英文文本。") | |
| elif args.checkpoint is None: | |
| st.error("未提供 checkpoint,无法执行翻译。请使用 --checkpoint 参数启动。") | |
| else: | |
| with st.spinner("Translating..."): | |
| try: | |
| evaluator = load_evaluator() | |
| translation = evaluator.translate_single(input_text.strip()) | |
| st.text_area("Chinese Translation", value=translation, height=200) | |
| except Exception as exc: | |
| st.error(f"模型加载或翻译失败:{exc}") | |
| st.markdown("---") | |
| st.caption("此页面用于展示前端界面;在未提供模型时,翻译功能会被禁用。") | |
| def launch_streamlit_process(args): | |
| script_path = Path(__file__).resolve() | |
| cmd = [sys.executable, "-m", "streamlit", "run", str(script_path), "--", "--streamlit-app", "--config", args.config] | |
| if args.checkpoint: | |
| cmd.extend(["--checkpoint", args.checkpoint]) | |
| if args.input: | |
| cmd.extend(["--input", args.input]) | |
| if args.output: | |
| cmd.extend(["--output", args.output]) | |
| subprocess.run(cmd, check=True) | |
| def _get_tokenizer_train_texts(config, allow_auto: bool = False) -> list[str] | None: | |
| dataset_name = _get_config(config, "data", "dataset_name") | |
| if dataset_name == "custom": | |
| custom = _get_config(config, "data", "custom") or {} | |
| data = load_custom_dataset( | |
| train_src=custom.get("train_src"), | |
| train_tgt=custom.get("train_tgt"), | |
| val_src=custom.get("val_src"), | |
| val_tgt=custom.get("val_tgt"), | |
| test_src=custom.get("test_src"), | |
| test_tgt=custom.get("test_tgt"), | |
| preprocessing_config=_get_config(config, "data", "preprocessing"), | |
| ) | |
| return list(data["train"]["src"]) + list(data["train"]["tgt"]) | |
| if not allow_auto: | |
| return None | |
| if dataset_name == "wmt": | |
| try: | |
| dataset = load_wmt_dataset( | |
| year=_get_config(config, "data", "wmt", "year"), | |
| language_pair=_get_config(config, "data", "wmt", "language_pair"), | |
| split="train", | |
| ) | |
| except Exception: | |
| dataset = load_wmt_dataset( | |
| year=_get_config(config, "data", "wmt", "year"), | |
| language_pair=_get_config(config, "data", "wmt", "language_pair"), | |
| split="validation", | |
| ) | |
| return list(dataset["src"]) + list(dataset["tgt"]) | |
| if dataset_name == "opus": | |
| try: | |
| dataset = load_opus_dataset( | |
| subset=_get_config(config, "data", "opus", "subset"), | |
| split="train", | |
| ) | |
| except Exception: | |
| dataset = load_opus_dataset( | |
| subset=_get_config(config, "data", "opus", "subset"), | |
| split="validation", | |
| ) | |
| return list(dataset["src"]) + list(dataset["tgt"]) | |
| return None | |
| def build_model_and_tokenizer(config, device): | |
| model_type = _get_config(config, "model", "type") | |
| if model_type == "transformer_scratch": | |
| tokenizer_config = _get_config(config, "tokenizer") or {} | |
| tokenizer_path = tokenizer_config.get("path") or tokenizer_config.get("tokenizer_path") | |
| tokenizer_type = tokenizer_config.get("type", "bpe") | |
| auto_train = bool(tokenizer_config.get("auto_train", False)) | |
| if tokenizer_type in {"bpe", "sentencepiece"} and not tokenizer_path: | |
| train_texts = _get_tokenizer_train_texts(config, allow_auto=auto_train) | |
| if train_texts is None: | |
| raise ValueError( | |
| "BPE tokenizer requires tokenizer.path or a local custom dataset with train texts. " | |
| "Automatic download from WMT/OPUS is disabled by default. " | |
| "Set tokenizer.auto_train=true to enable it, or provide tokenizer.path/pretrained tokenizer." | |
| ) | |
| tokenizer = build_tokenizer(tokenizer_config, train_texts=train_texts) | |
| else: | |
| tokenizer = build_tokenizer(tokenizer_config) | |
| model = TransformerTranslationModel( | |
| src_vocab_size=tokenizer.vocab_size, | |
| tgt_vocab_size=tokenizer.vocab_size, | |
| d_model=_get_config(config, "model", "transformer", "d_model"), | |
| nhead=_get_config(config, "model", "transformer", "nhead"), | |
| num_encoder_layers=_get_config(config, "model", "transformer", "num_encoder_layers"), | |
| num_decoder_layers=_get_config(config, "model", "transformer", "num_decoder_layers"), | |
| dim_feedforward=_get_config(config, "model", "transformer", "dim_feedforward"), | |
| dropout=_get_config(config, "model", "transformer", "dropout"), | |
| activation=_get_config(config, "model", "transformer", "activation"), | |
| max_seq_len=_get_config(config, "model", "transformer", "max_seq_len"), | |
| use_flash_attention=_get_config(config, "model", "transformer", "use_flash_attention"), | |
| use_rotary_embedding=_get_config(config, "model", "transformer", "use_rotary_embedding"), | |
| pre_norm=_get_config(config, "model", "transformer", "pre_norm"), | |
| pad_id=tokenizer.pad_token_id, | |
| ) | |
| return model.to(device), tokenizer | |
| model, hf_tokenizer = load_pretrained_model( | |
| config.model.pretrained.model_name, | |
| config.model.pretrained.src_lang, | |
| config.model.pretrained.tgt_lang, | |
| device=str(device), | |
| ) | |
| tokenizer = TokenizerWrapper( | |
| hf_tokenizer, | |
| pad_token=getattr(hf_tokenizer, "pad_token", "<pad>"), | |
| unk_token=getattr(hf_tokenizer, "unk_token", "<unk>"), | |
| bos_token=getattr(hf_tokenizer, "bos_token", "<s>"), | |
| eos_token=getattr(hf_tokenizer, "eos_token", "</s>"), | |
| ) | |
| return model, tokenizer | |
| def load_checkpoint(model, checkpoint_path, device): | |
| checkpoint = torch.load(checkpoint_path, map_location=device) | |
| if isinstance(checkpoint, dict): | |
| if "model_state_dict" in checkpoint: | |
| model.load_state_dict(checkpoint["model_state_dict"]) | |
| elif "state_dict" in checkpoint: | |
| model.load_state_dict(checkpoint["state_dict"]) | |
| else: | |
| try: | |
| model.load_state_dict(checkpoint) | |
| except Exception as exc: | |
| raise ValueError("Checkpoint does not contain a valid model state dict") from exc | |
| else: | |
| raise ValueError("Unsupported checkpoint format") | |
| return model | |
| def main(): | |
| args, cli_overrides = parse_args() | |
| print("=" * 60) | |
| print(" EasyTranslate - Translation") | |
| print("=" * 60) | |
| if args.web and not args.streamlit_app: | |
| launch_streamlit_process(args) | |
| return | |
| if args.streamlit_app: | |
| launch_streamlit_app(args) | |
| return | |
| config = _load_config(args.config, cli_overrides) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model, tokenizer = build_model_and_tokenizer(config, device) | |
| model = load_checkpoint(model, args.checkpoint, device) | |
| evaluator = Evaluator(model, tokenizer, config) | |
| if args.input and args.output: | |
| translate_file(evaluator, args.input, args.output) | |
| else: | |
| interactive_translate(evaluator) | |
| if __name__ == "__main__": | |
| main() | |