Spaces:
Paused
Paused
Initial Space creation from repo-embedded image
Browse files- README.md +6 -5
- app.py +106 -0
- app_logic.py +169 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Image To Space
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 5.34.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 13 |
+
|
app.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from app_logic import (
|
| 3 |
+
build_space_from_image,
|
| 4 |
+
get_username_from_token,
|
| 5 |
+
preview_image_contents,
|
| 6 |
+
update_file_preview
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
def main_ui():
|
| 10 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="Image-to-Space Builder") as demo:
|
| 11 |
+
gr.Markdown(
|
| 12 |
+
"""
|
| 13 |
+
# πΌοΈ Image-to-Space Builder
|
| 14 |
+
## Create a new Hugging Face Space directly from a repo-embedded image.
|
| 15 |
+
|
| 16 |
+
This tool extracts the file structure and content from an encrypted image (created with a tool like KeyLock)
|
| 17 |
+
and uses it to build and deploy a new Hugging Face Space.
|
| 18 |
+
"""
|
| 19 |
+
)
|
| 20 |
+
file_data_state = gr.State([])
|
| 21 |
+
|
| 22 |
+
with gr.Row():
|
| 23 |
+
with gr.Column(scale=1):
|
| 24 |
+
gr.Markdown("### 1. Upload & Preview Image")
|
| 25 |
+
repo_image_input = gr.Image(label="Repo-Embedded Image (PNG)", type="pil")
|
| 26 |
+
image_password_input = gr.Textbox(label="Image Decryption Password", type="password")
|
| 27 |
+
|
| 28 |
+
preview_button = gr.Button("Preview Contents", variant="secondary")
|
| 29 |
+
|
| 30 |
+
gr.Markdown("---")
|
| 31 |
+
gr.Markdown("### 2. Configure Your New Space")
|
| 32 |
+
api_token_input = gr.Textbox(
|
| 33 |
+
label="Hugging Face API Token (hf_xxx)",
|
| 34 |
+
type="password",
|
| 35 |
+
placeholder="Enter your write-permission token"
|
| 36 |
+
)
|
| 37 |
+
owner_input = gr.Textbox(
|
| 38 |
+
label="Owner (Username/Org)",
|
| 39 |
+
placeholder="Autofills from token if left blank"
|
| 40 |
+
)
|
| 41 |
+
space_name_input = gr.Textbox(label="New Space Name", placeholder="e.g., my-new-app")
|
| 42 |
+
sdk_input = gr.Dropdown(
|
| 43 |
+
label="Space SDK",
|
| 44 |
+
choices=["gradio", "streamlit", "docker", "static"],
|
| 45 |
+
value="gradio"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
gr.Markdown("---")
|
| 49 |
+
gr.Markdown("### 3. Build It!")
|
| 50 |
+
create_button = gr.Button("Create Space from Image", variant="primary")
|
| 51 |
+
|
| 52 |
+
with gr.Column(scale=2):
|
| 53 |
+
gr.Markdown("### Status & Result")
|
| 54 |
+
output_status_md = gr.Markdown(label="Status")
|
| 55 |
+
with gr.Column(visible=False) as file_browser_ui:
|
| 56 |
+
gr.Markdown("### Image Contents Preview")
|
| 57 |
+
file_selector_dd = gr.Dropdown(label="Select file to preview", interactive=True)
|
| 58 |
+
file_preview_code = gr.Code(language="python", interactive=False, label="File Content")
|
| 59 |
+
|
| 60 |
+
def autofill_owner_if_empty(api_token, current_owner):
|
| 61 |
+
if not current_owner.strip():
|
| 62 |
+
return get_username_from_token(api_token)
|
| 63 |
+
return current_owner
|
| 64 |
+
|
| 65 |
+
api_token_input.blur(
|
| 66 |
+
fn=autofill_owner_if_empty,
|
| 67 |
+
inputs=[api_token_input, owner_input],
|
| 68 |
+
outputs=[owner_input]
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
preview_button.click(
|
| 72 |
+
fn=preview_image_contents,
|
| 73 |
+
inputs=[repo_image_input, image_password_input],
|
| 74 |
+
outputs=[
|
| 75 |
+
output_status_md,
|
| 76 |
+
file_browser_ui,
|
| 77 |
+
file_selector_dd,
|
| 78 |
+
file_preview_code,
|
| 79 |
+
file_data_state
|
| 80 |
+
]
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
file_selector_dd.change(
|
| 84 |
+
fn=update_file_preview,
|
| 85 |
+
inputs=[file_selector_dd, file_data_state],
|
| 86 |
+
outputs=[file_preview_code]
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
create_button.click(
|
| 90 |
+
fn=build_space_from_image,
|
| 91 |
+
inputs=[
|
| 92 |
+
api_token_input,
|
| 93 |
+
repo_image_input,
|
| 94 |
+
image_password_input,
|
| 95 |
+
space_name_input,
|
| 96 |
+
owner_input,
|
| 97 |
+
sdk_input
|
| 98 |
+
],
|
| 99 |
+
outputs=[output_status_md]
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
return demo
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
demo = main_ui()
|
| 106 |
+
demo.launch(show_error=True)
|
app_logic.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import tempfile
|
| 3 |
+
import gradio as gr
|
| 4 |
+
from huggingface_hub import create_repo, upload_folder, whoami
|
| 5 |
+
import logging
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from keylock import core as keylock_core
|
| 9 |
+
from repo_to_md.core import markdown_to_files
|
| 10 |
+
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=logging.INFO,
|
| 13 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 14 |
+
)
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
def get_username_from_token(api_token: str):
|
| 18 |
+
if not api_token:
|
| 19 |
+
logger.info("API token is empty, cannot fetch username.")
|
| 20 |
+
return ""
|
| 21 |
+
try:
|
| 22 |
+
logger.info("Attempting to fetch username from provided API token.")
|
| 23 |
+
user_info = whoami(token=api_token)
|
| 24 |
+
username = user_info.get('name')
|
| 25 |
+
if username:
|
| 26 |
+
logger.info(f"Successfully fetched username: {username}")
|
| 27 |
+
return username
|
| 28 |
+
else:
|
| 29 |
+
logger.warning("Token was valid, but no username found in whoami() response.")
|
| 30 |
+
return ""
|
| 31 |
+
except Exception as e:
|
| 32 |
+
logger.error(f"Failed to validate token and fetch username: {e}")
|
| 33 |
+
return ""
|
| 34 |
+
|
| 35 |
+
def _get_api_token(ui_token_from_textbox=None):
|
| 36 |
+
env_token = os.getenv('HF_TOKEN')
|
| 37 |
+
if env_token: return env_token, None
|
| 38 |
+
if ui_token_from_textbox: return ui_token_from_textbox, None
|
| 39 |
+
return None, "Error: Hugging Face API token not provided."
|
| 40 |
+
|
| 41 |
+
def extract_markdown_from_image(repo_image: Image.Image, image_password: str):
|
| 42 |
+
if not repo_image:
|
| 43 |
+
return None, "Error: Please upload a repository image."
|
| 44 |
+
password_to_use = image_password if image_password is not None else ""
|
| 45 |
+
try:
|
| 46 |
+
logger.info("Starting extraction of markdown from image...")
|
| 47 |
+
result = keylock_core.extract_repo_from_image(repo_image, password_to_use)
|
| 48 |
+
markdown_content = result.get('markdown')
|
| 49 |
+
if not markdown_content:
|
| 50 |
+
return None, "Error: Extraction succeeded, but no markdown content was found in the image."
|
| 51 |
+
logger.info("Successfully extracted markdown content.")
|
| 52 |
+
return markdown_content, result.get('status', "Extraction successful.")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"Failed to extract from image: {e}")
|
| 55 |
+
return None, f"Error during extraction: {e}"
|
| 56 |
+
|
| 57 |
+
def parse_markdown_for_build(markdown_input):
|
| 58 |
+
space_info = {"files": []}
|
| 59 |
+
current_file_path = None; current_file_content_lines = []
|
| 60 |
+
in_file_definition = False; in_code_block = False
|
| 61 |
+
lines = markdown_input.strip().split("\n")
|
| 62 |
+
for line_content_orig in lines:
|
| 63 |
+
line_content_stripped = line_content_orig.strip()
|
| 64 |
+
if line_content_stripped.startswith("### File:"):
|
| 65 |
+
if current_file_path and in_file_definition:
|
| 66 |
+
space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines)})
|
| 67 |
+
current_file_path = line_content_stripped.replace("### File:", "").strip()
|
| 68 |
+
current_file_content_lines = []; in_file_definition = True; in_code_block = False
|
| 69 |
+
continue
|
| 70 |
+
if not in_file_definition: continue
|
| 71 |
+
if line_content_stripped.startswith("```"):
|
| 72 |
+
in_code_block = not in_code_block
|
| 73 |
+
continue
|
| 74 |
+
current_file_content_lines.append(line_content_orig)
|
| 75 |
+
if current_file_path and in_file_definition:
|
| 76 |
+
space_info["files"].append({"path": current_file_path, "content": "\n".join(current_file_content_lines)})
|
| 77 |
+
space_info["files"] = [f for f in space_info["files"] if f.get("path")]
|
| 78 |
+
return space_info
|
| 79 |
+
|
| 80 |
+
def _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui):
|
| 81 |
+
if not space_name_ui: return None, "Error: Space Name cannot be empty."
|
| 82 |
+
if "/" in space_name_ui: return None, "Error: Space Name should not contain '/'. Use the Owner field."
|
| 83 |
+
final_owner = owner_ui
|
| 84 |
+
if not final_owner:
|
| 85 |
+
resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
|
| 86 |
+
if token_err: return None, token_err
|
| 87 |
+
try:
|
| 88 |
+
user_info = whoami(token=resolved_api_token)
|
| 89 |
+
final_owner = user_info.get('name')
|
| 90 |
+
except Exception as e:
|
| 91 |
+
return None, f"Error retrieving username from token: {e}. Please specify the Owner manually."
|
| 92 |
+
if not final_owner: return None, "Error: Owner could not be determined."
|
| 93 |
+
return f"{final_owner}/{space_name_ui}", None
|
| 94 |
+
|
| 95 |
+
def create_space(ui_api_token_from_textbox, space_name_ui, owner_ui, sdk_ui, markdown_input):
|
| 96 |
+
repo_id, err = _determine_repo_id(ui_api_token_from_textbox, space_name_ui, owner_ui)
|
| 97 |
+
if err: return err
|
| 98 |
+
try:
|
| 99 |
+
resolved_api_token, token_err = _get_api_token(ui_api_token_from_textbox)
|
| 100 |
+
if token_err: return token_err
|
| 101 |
+
space_info = parse_markdown_for_build(markdown_input)
|
| 102 |
+
if not space_info["files"]: return "Error: No files were found after parsing the markdown from the image."
|
| 103 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 104 |
+
repo_staging_path = Path(temp_dir) / "repo_staging"
|
| 105 |
+
repo_staging_path.mkdir()
|
| 106 |
+
for file_info in space_info["files"]:
|
| 107 |
+
file_path_abs = repo_staging_path / file_info["path"]
|
| 108 |
+
file_path_abs.parent.mkdir(parents=True, exist_ok=True)
|
| 109 |
+
with open(file_path_abs, "w", encoding="utf-8") as f:
|
| 110 |
+
f.write(file_info["content"])
|
| 111 |
+
create_repo(repo_id=repo_id, token=resolved_api_token, repo_type="space", space_sdk=sdk_ui, exist_ok=True)
|
| 112 |
+
upload_folder(
|
| 113 |
+
repo_id=repo_id,
|
| 114 |
+
folder_path=str(repo_staging_path),
|
| 115 |
+
path_in_repo=".",
|
| 116 |
+
token=resolved_api_token,
|
| 117 |
+
repo_type="space",
|
| 118 |
+
commit_message="Initial Space creation from repo-embedded image"
|
| 119 |
+
)
|
| 120 |
+
return f"**Success!** Space created or updated: [{repo_id}](https://huggingface.co/spaces/{repo_id})"
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.exception(f"Error in create_space for {repo_id}:")
|
| 123 |
+
return f"Error during Space creation: {str(e)}"
|
| 124 |
+
|
| 125 |
+
def build_space_from_image(api_token, repo_image, image_password, new_space_name, owner, sdk):
|
| 126 |
+
markdown_string, status_msg = extract_markdown_from_image(repo_image, image_password)
|
| 127 |
+
if not markdown_string:
|
| 128 |
+
return status_msg
|
| 129 |
+
result = create_space(api_token, new_space_name, owner, sdk, markdown_string)
|
| 130 |
+
return result
|
| 131 |
+
|
| 132 |
+
def preview_image_contents(repo_image, image_password):
|
| 133 |
+
markdown_string, status_msg = extract_markdown_from_image(repo_image, image_password)
|
| 134 |
+
if not markdown_string:
|
| 135 |
+
return status_msg, gr.update(visible=False), gr.update(choices=[], value=None), "", []
|
| 136 |
+
try:
|
| 137 |
+
file_data, _ = markdown_to_files(markdown_string)
|
| 138 |
+
if isinstance(file_data, str) or not file_data:
|
| 139 |
+
raise ValueError("Markdown parsing failed or returned no files.")
|
| 140 |
+
|
| 141 |
+
filenames = [f['filename'] for f in file_data]
|
| 142 |
+
if not filenames:
|
| 143 |
+
status = "β
Preview generated, but no files were found in the markdown structure."
|
| 144 |
+
return status, gr.update(visible=False), gr.update(choices=[], value=None), "", []
|
| 145 |
+
|
| 146 |
+
first_filename = filenames
|
| 147 |
+
first_content = file_data[0]['content']
|
| 148 |
+
|
| 149 |
+
status = f"β
Preview generated successfully. Found {len(filenames)} file(s)."
|
| 150 |
+
|
| 151 |
+
return (
|
| 152 |
+
status,
|
| 153 |
+
gr.update(visible=True),
|
| 154 |
+
gr.update(choices=filenames, value=first_filename),
|
| 155 |
+
first_content,
|
| 156 |
+
file_data
|
| 157 |
+
)
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error parsing markdown for preview: {e}", exc_info=True)
|
| 160 |
+
error_msg = f"β Error: Could not parse files from the image's content. {e}"
|
| 161 |
+
return error_msg, gr.update(visible=False), gr.update(choices=[], value=None), "", []
|
| 162 |
+
|
| 163 |
+
def update_file_preview(selected_filename, all_file_data):
|
| 164 |
+
if not selected_filename or not all_file_data:
|
| 165 |
+
return ""
|
| 166 |
+
selected_file = next((f for f in all_file_data if f['filename'] == selected_filename), None)
|
| 167 |
+
if selected_file:
|
| 168 |
+
return selected_file.get('content', 'File content not found.')
|
| 169 |
+
return f"Select file to view contents."
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
git+https://github.com/broadfield-dev/repo_to_md.git
|
| 2 |
+
git+https://github.com/broadfield-dev/keylock-core.git@dev
|