Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1127 +1,380 @@
|
|
| 1 |
-
# app.py
|
| 2 |
import gradio as gr
|
| 3 |
-
import
|
| 4 |
-
import json
|
| 5 |
-
# Remove direct requests import, will use model_logic
|
| 6 |
-
# import requests
|
| 7 |
-
import os
|
| 8 |
import tempfile
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
{
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
Only the latest version of each file mentioned throughout the chat will be used for the final output.
|
| 66 |
-
Filenames in the '### File:' line should be clean paths (e.g., 'src/app.py', 'Dockerfile') and should NOT include Markdown backticks around the filename itself.
|
| 67 |
-
If the user provides existing code (e.g., by pasting a Markdown structure), and asks for modifications, ensure your response includes the complete, modified versions of ONLY the files that changed, using the ### File: format. Unchanged files do not need to be repeated by you. The system will merge your changes with the prior state.
|
| 68 |
-
If the user asks to delete a file, simply omit it from your next full ### File: list.
|
| 69 |
-
If no code is provided, assist the user with their tasks.
|
| 70 |
-
"""
|
| 71 |
-
|
| 72 |
-
# --- Core Utility, Parsing, Export functions (mostly unchanged) ---
|
| 73 |
-
def escape_html_for_markdown(text):
|
| 74 |
-
if not isinstance(text, str): return ""
|
| 75 |
-
# Minimal escaping, expand if needed
|
| 76 |
-
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def _infer_lang_from_filename(filename):
|
| 80 |
-
if not filename: return "plaintext"
|
| 81 |
-
if '.' in filename:
|
| 82 |
-
ext = filename.split('.')[-1].lower()
|
| 83 |
-
mapping = {
|
| 84 |
-
'py': 'python', 'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript',
|
| 85 |
-
'html': 'html', 'htm': 'html', 'css': 'css', 'scss': 'scss', 'sass': 'sass', 'less': 'less',
|
| 86 |
-
'json': 'json', 'xml': 'xml', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml',
|
| 87 |
-
'md': 'markdown', 'rst': 'rst',
|
| 88 |
-
'sh': 'bash', 'bash': 'bash', 'zsh': 'bash', 'bat': 'batch', 'cmd': 'batch', 'ps1': 'powershell',
|
| 89 |
-
'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp', 'cs': 'csharp', 'java': 'java',
|
| 90 |
-
'rb': 'ruby', 'php': 'php', 'go': 'go', 'rs': 'rust', 'swift': 'swift', 'kt': 'kotlin', 'kts': 'kotlin',
|
| 91 |
-
'sql': 'sql', 'dockerfile': 'docker', 'tf': 'terraform', 'hcl': 'terraform',
|
| 92 |
-
'txt': 'plaintext', 'log': 'plaintext', 'ini': 'ini', 'conf': 'plaintext', 'cfg': 'plaintext',
|
| 93 |
-
'csv': 'plaintext', 'tsv': 'plaintext', 'err': 'plaintext',
|
| 94 |
-
'.env': 'plaintext', '.gitignore': 'plaintext', '.npmrc': 'plaintext', '.gitattributes': 'plaintext',
|
| 95 |
-
'makefile': 'makefile',
|
| 96 |
-
}
|
| 97 |
-
return mapping.get(ext, "plaintext")
|
| 98 |
-
base_filename = os.path.basename(filename)
|
| 99 |
-
if base_filename == 'Dockerfile': return 'docker'
|
| 100 |
-
if base_filename == 'Makefile': return 'makefile'
|
| 101 |
-
if base_filename.startswith('.'): return 'plaintext'
|
| 102 |
-
return "plaintext"
|
| 103 |
-
|
| 104 |
-
def _clean_filename(filename_line_content):
|
| 105 |
-
text = filename_line_content.strip()
|
| 106 |
-
text = re.sub(r'[`\*_]+', '', text) # Remove markdown emphasis characters
|
| 107 |
-
# Try to match a valid-looking path first (allow spaces in folder/file names if quoted or part of a segment)
|
| 108 |
-
path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
|
| 109 |
-
if path_match:
|
| 110 |
-
# Further clean if it looks like "path/to/file (description)"
|
| 111 |
-
# Corrected split index was already correct in the original code, just ensure it's applied
|
| 112 |
-
parts = re.split(r'\s*\(', path_match.group(1).strip(), 1)
|
| 113 |
-
return parts[0].strip() if parts else ""
|
| 114 |
-
|
| 115 |
-
# Fallback for more complex lines, like "### File: `src/app.py` (main application)"
|
| 116 |
-
backtick_match = re.search(r'`([^`]+)`', text)
|
| 117 |
-
if backtick_match:
|
| 118 |
-
potential_fn = backtick_match.group(1).strip()
|
| 119 |
-
# Corrected split index was already correct
|
| 120 |
-
parts = re.split(r'\s*\(|\s{2,}', potential_fn, 1)
|
| 121 |
-
cleaned_fn = parts[0].strip() if parts else ""
|
| 122 |
-
cleaned_fn = cleaned_fn.strip('`\'":;,') # Clean common wrapping chars
|
| 123 |
-
if cleaned_fn: return cleaned_fn
|
| 124 |
-
|
| 125 |
-
# Final fallback
|
| 126 |
-
parts = re.split(r'\s*\(|\s{2,}', text, 1)
|
| 127 |
-
filename_candidate = parts[0].strip() if parts else text.strip()
|
| 128 |
-
filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
|
| 129 |
-
return filename_candidate if filename_candidate else text.strip()
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=None):
|
| 133 |
-
"""
|
| 134 |
-
Parses a single bot message content string to find file blocks and updates the state.
|
| 135 |
-
Assumes existing_files_state is the current state *before* this message.
|
| 136 |
-
"""
|
| 137 |
-
# This function takes state as an argument and returns new state, it doesn't need 'global'
|
| 138 |
-
latest_blocks_dict = {}
|
| 139 |
-
if existing_files_state:
|
| 140 |
-
# Copy existing blocks, except for potential structure blocks that might be overwritten
|
| 141 |
-
for block in existing_files_state:
|
| 142 |
-
if not block.get("is_structure_block"):
|
| 143 |
-
latest_blocks_dict[block["filename"]] = block.copy()
|
| 144 |
-
# Keep existing structure block for now, it might be replaced below
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
results = {"parsed_code_blocks": [], "preview_md": "", "default_selected_filenames": [], "error_message": None}
|
| 148 |
-
content = latest_bot_message_content or ""
|
| 149 |
-
|
| 150 |
-
file_pattern = re.compile(r"### File:\s*(?P<filename_line>[^\n]+)\n(?:```(?P<lang>[\w\.\-\+]*)\n(?P<code>[\s\S]*?)\n```|(?P<binary_msg>\[Binary file(?: - [^\]]+)?\]))")
|
| 151 |
-
structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
|
| 152 |
-
|
| 153 |
-
# Process the latest bot message for updates to file blocks
|
| 154 |
-
structure_match = structure_pattern.search(content)
|
| 155 |
-
if structure_match:
|
| 156 |
-
# Add/Overwrite the structure block from the latest response
|
| 157 |
-
latest_blocks_dict["File Structure (original)"] = {"filename": "File Structure (original)", "language": structure_match.group("struct_lang") or "plaintext", "code": structure_match.group("structure_code").strip(), "is_binary": False, "is_structure_block": True}
|
| 158 |
-
else:
|
| 159 |
-
# If the latest message *doesn't* have a structure block, keep the previous one if it existed
|
| 160 |
-
existing_structure_block = next((b for b in (existing_files_state or []) if b.get("is_structure_block")), None)
|
| 161 |
-
if existing_structure_block:
|
| 162 |
-
latest_blocks_dict["File Structure (original)"] = existing_structure_block.copy()
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
# Find all file blocks in the latest message
|
| 166 |
-
current_message_file_blocks = {}
|
| 167 |
-
for match in file_pattern.finditer(content):
|
| 168 |
-
filename = _clean_filename(match.group("filename_line"))
|
| 169 |
-
if not filename: continue
|
| 170 |
-
lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
|
| 171 |
-
item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
|
| 172 |
-
if code_block is not None:
|
| 173 |
-
item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
|
| 174 |
-
elif binary_msg is not None:
|
| 175 |
-
item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
|
| 176 |
-
else: continue # Should not happen with the regex
|
| 177 |
-
current_message_file_blocks[filename] = item_data
|
| 178 |
-
|
| 179 |
-
# Update latest_blocks_dict with blocks from the current message
|
| 180 |
-
# Any file mentioned in the latest message replaces its old version
|
| 181 |
-
latest_blocks_dict.update(current_message_file_blocks)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# Convert dictionary values back to a list
|
| 185 |
-
current_parsed_blocks = list(latest_blocks_dict.values())
|
| 186 |
-
# Sort: structure block first, then files alphabetically
|
| 187 |
-
current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
| 188 |
-
|
| 189 |
-
# Update the global cache outside this function if needed, or pass it back
|
| 190 |
-
# For now, let's return the new state and let the caller update the cache.
|
| 191 |
-
results["parsed_code_blocks"] = current_parsed_blocks
|
| 192 |
-
results["default_selected_filenames"] = [b["filename"] for b in current_parsed_blocks if not b.get("is_structure_block")]
|
| 193 |
-
return results
|
| 194 |
-
|
| 195 |
-
def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
|
| 196 |
-
# This function remains largely the same, using the provided parsed_blocks_for_export
|
| 197 |
-
# It takes state as an argument and doesn't need 'global'
|
| 198 |
-
results = {"output_str": "", "error_message": None, "download_filepath": None}
|
| 199 |
-
# Filter out structure blocks for file listing/export content
|
| 200 |
-
exportable_blocks_content = [b for b in parsed_blocks_for_export if not b.get("is_structure_block") and not b.get("is_binary") and not (b.get("code", "").startswith("[Error loading content:") or b.get("code", "").startswith("[Binary or Skipped file]"))]
|
| 201 |
-
binary_blocks_content = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
|
| 202 |
-
|
| 203 |
-
# Collect all filenames (including binary/error ones) for the structure list
|
| 204 |
-
all_filenames_in_state = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block"))))
|
| 205 |
-
|
| 206 |
-
if not all_filenames_in_state and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
|
| 207 |
-
results["output_str"] = f"# Space: {space_line_name_for_md}\n## File Structure\n{bbb}\n📁 Root\n{bbb}\n\n*No files to list in structure or export.*"
|
| 208 |
-
try:
|
| 209 |
-
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
|
| 210 |
-
tmpfile.write(results["output_str"]); results["download_filepath"] = tmpfile.name
|
| 211 |
-
except Exception as e: print(f"Error creating temp file for empty export: {e}")
|
| 212 |
-
return results
|
| 213 |
-
|
| 214 |
-
output_lines = [f"# Space: {space_line_name_for_md}"]
|
| 215 |
-
|
| 216 |
-
# Add File Structure block if it exists in parsed blocks
|
| 217 |
-
structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
|
| 218 |
-
if structure_block:
|
| 219 |
-
output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
|
| 220 |
-
else:
|
| 221 |
-
# If no structure block from AI, generate a simple one from detected files
|
| 222 |
-
output_lines.extend(["## File Structure", bbb, "📁 Root"])
|
| 223 |
-
if all_filenames_in_state:
|
| 224 |
-
for fname in all_filenames_in_state: output_lines.append(f" 📄 {fname}")
|
| 225 |
-
output_lines.extend([bbb, ""])
|
| 226 |
-
|
| 227 |
-
output_lines.append("Below are the contents of all files in the space:\n")
|
| 228 |
-
exported_content = False
|
| 229 |
-
|
| 230 |
-
# Determine which files to export content for based on selection or default
|
| 231 |
-
# Exportable content blocks only
|
| 232 |
-
files_to_export_content = []
|
| 233 |
-
if selected_filenames:
|
| 234 |
-
files_to_export_content = [b for b in exportable_blocks_content if b["filename"] in selected_filenames]
|
| 235 |
-
else:
|
| 236 |
-
files_to_export_content = exportable_blocks_content # Export all content blocks by default
|
| 237 |
-
|
| 238 |
-
# Add binary/error blocks if they were selected or if exporting all (and they exist)
|
| 239 |
-
# Binary/error blocks are listed in the structure, but their *content* is just the marker string
|
| 240 |
-
binary_error_blocks_to_export = []
|
| 241 |
-
if selected_filenames:
|
| 242 |
-
binary_error_blocks_to_export = [b for b in binary_blocks_content if b["filename"] in selected_filenames]
|
| 243 |
-
elif binary_blocks_content:
|
| 244 |
-
binary_error_blocks_to_export = binary_blocks_content # Include all binary/error if exporting all
|
| 245 |
-
|
| 246 |
-
# Combine and sort all blocks whose content/marker should be included
|
| 247 |
-
all_blocks_to_export_content = sorted(files_to_export_content + binary_error_blocks_to_export, key=lambda b: b["filename"])
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
for block in all_blocks_to_export_content:
|
| 251 |
-
output_lines.append(f"### File: {block['filename']}")
|
| 252 |
-
if block.get('is_binary') or block.get("code", "").startswith("[Binary file") or block.get("code", "").startswith("[Error loading content:") or block.get("code", "").startswith("[Binary or Skipped file]"):
|
| 253 |
-
# For binary/error placeholders, just add the marker line
|
| 254 |
-
output_lines.append(block.get('code','[Binary or Skipped file]'))
|
| 255 |
-
else:
|
| 256 |
-
# For actual code/text content
|
| 257 |
-
output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
|
| 258 |
-
output_lines.append(""); exported_content = True
|
| 259 |
-
|
| 260 |
-
if not exported_content and not all_filenames_in_state: output_lines.append("*No files in state.*")
|
| 261 |
-
elif not exported_content: output_lines.append("*No files with editable content are in the state or selected.*") # Message updated
|
| 262 |
-
|
| 263 |
-
final_output_str = "\n".join(output_lines)
|
| 264 |
-
results["output_str"] = final_output_str
|
| 265 |
-
try:
|
| 266 |
-
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
|
| 267 |
-
tmpfile.write(final_output_str); results["download_filepath"] = tmpfile.name
|
| 268 |
-
except Exception as e: print(f"Error creating temp file: {e}"); results["error_message"] = "Could not prepare file for download."
|
| 269 |
-
return results
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_message=None):
|
| 273 |
-
# This function is fine as is, it produces standard OpenAI format
|
| 274 |
-
messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
|
| 275 |
-
for user_msg, bot_msg in gr_history:
|
| 276 |
-
if user_msg: messages.append({"role": "user", "content": user_msg})
|
| 277 |
-
# Ensure bot_msg is not None or empty before adding
|
| 278 |
-
if bot_msg and isinstance(bot_msg, str): messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
|
| 279 |
-
if current_user_message: messages.append({"role": "user", "content": current_user_message})
|
| 280 |
-
return messages
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
def _generate_ui_outputs_from_cache(owner, space_name):
|
| 284 |
-
# Declare global at the top
|
| 285 |
-
global parsed_code_blocks_state_cache
|
| 286 |
-
# This function remains largely the same, generating UI previews and the export MD
|
| 287 |
-
preview_md_val = "*No files in cache to display.*"
|
| 288 |
-
formatted_md_val = f"# Space: {owner}/{space_name}\n## File Structure\n{bbb}\n📁 Root\n{bbb}\n\n*No files in cache.*" if owner or space_name else "*Load or define a Space to see its Markdown structure.*"
|
| 289 |
-
download_file = None
|
| 290 |
-
|
| 291 |
-
if parsed_code_blocks_state_cache:
|
| 292 |
-
preview_md_lines = ["## Detected/Updated Files & Content (Latest Versions):"]
|
| 293 |
-
for block in parsed_code_blocks_state_cache:
|
| 294 |
-
preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
|
| 295 |
-
if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
|
| 296 |
-
elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
|
| 297 |
-
elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
|
| 298 |
-
else: preview_md_lines.append("\n")
|
| 299 |
-
|
| 300 |
-
# Handle content display
|
| 301 |
-
content = block.get('code', '')
|
| 302 |
-
if block.get('is_binary') or content.startswith("["): # Treat errors/skipped as binary for preview display
|
| 303 |
-
preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
|
| 304 |
-
elif block.get('is_structure_block'):
|
| 305 |
-
preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
|
| 306 |
-
else:
|
| 307 |
-
preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
preview_md_val = "\n".join(preview_md_lines)
|
| 311 |
-
space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
|
| 312 |
-
|
| 313 |
-
# _export_selected_logic handles selecting which files to include in the export MD
|
| 314 |
-
# Passing None means export all non-structure/non-binary/non-error content + list all files in structure
|
| 315 |
-
export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache)
|
| 316 |
-
formatted_md_val = export_result["output_str"]
|
| 317 |
-
download_file = export_result["download_filepath"]
|
| 318 |
-
|
| 319 |
-
return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
# --- Refactored Chat Submit Handler ---
|
| 323 |
-
def handle_chat_submit(user_message, chat_history, api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
|
| 324 |
-
# Declare global at the top
|
| 325 |
-
global parsed_code_blocks_state_cache
|
| 326 |
-
_chat_msg_in = ""
|
| 327 |
-
_chat_hist = list(chat_history)
|
| 328 |
-
_status = "Initializing..."
|
| 329 |
-
_detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
|
| 330 |
-
|
| 331 |
-
# --- Before sending to AI: Parse existing files from the current formatted markdown ---
|
| 332 |
-
# This ensures the AI is aware of the *current* state including user edits
|
| 333 |
-
# or files loaded from HF, before it generates its response.
|
| 334 |
-
# Only do this on a new user message
|
| 335 |
-
if user_message and _current_formatted_markdown:
|
| 336 |
-
try:
|
| 337 |
-
parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
|
| 338 |
-
# Update cache with files parsed from the markdown.
|
| 339 |
-
# Structure block from AI is volatile, always prefer structure from latest AI message.
|
| 340 |
-
# Files from markdown overwrite any previous file blocks.
|
| 341 |
-
new_cache_state = []
|
| 342 |
-
# Add structure block from *current cache* if it exists, it will be replaced if the AI provides a new one
|
| 343 |
-
existing_structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
|
| 344 |
-
if existing_structure_block:
|
| 345 |
-
new_cache_state.append(existing_structure_block.copy()) # Add copy
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
for f_info in parsed_from_md.get("files", []):
|
| 349 |
-
# Only add if it has a path and isn't the structure block representation placeholder
|
| 350 |
-
if f_info.get("path") and f_info["path"] != "File Structure (original)":
|
| 351 |
-
# Check if it's a binary representation string
|
| 352 |
-
is_binary_repr = isinstance(f_info.get("content"), str) and (f_info["content"].startswith("[Binary file") or f_info["content"].startswith("[Error loading content:") or f_info["content"].startswith("[Binary or Skipped file]"))
|
| 353 |
-
# Check if a block with this filename already exists in new_cache_state and replace it
|
| 354 |
-
found_existing = False
|
| 355 |
-
for i, block in enumerate(new_cache_state):
|
| 356 |
-
if block["filename"] == f_info["path"] and not block.get("is_structure_block"): # Only replace non-structure blocks
|
| 357 |
-
new_cache_state[i] = {
|
| 358 |
-
"filename": f_info["path"],
|
| 359 |
-
"code": f_info.get("content", ""),
|
| 360 |
-
"language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
|
| 361 |
-
"is_binary": is_binary_repr,
|
| 362 |
-
"is_structure_block": False
|
| 363 |
-
}
|
| 364 |
-
found_existing = True
|
| 365 |
-
break
|
| 366 |
-
if not found_existing:
|
| 367 |
-
new_cache_state.append({
|
| 368 |
-
"filename": f_info["path"],
|
| 369 |
-
"code": f_info.get("content", ""),
|
| 370 |
-
"language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
|
| 371 |
-
"is_binary": is_binary_repr,
|
| 372 |
-
"is_structure_block": False
|
| 373 |
-
})
|
| 374 |
-
|
| 375 |
-
# Sort the updated cache state
|
| 376 |
-
new_cache_state.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
| 377 |
-
parsed_code_blocks_state_cache = new_cache_state # Update global cache
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
except Exception as e:
|
| 381 |
-
# Log error but don't block chat submission
|
| 382 |
-
print(f"Error parsing formatted markdown before chat submit: {e}")
|
| 383 |
-
# Optionally update status: _status = f"Warning: Error parsing current files: {e}. Sending message anyway."
|
| 384 |
-
|
| 385 |
-
# --- End of pre-chat parsing ---
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
if not user_message.strip():
|
| 389 |
-
_status = "Cannot send an empty message."
|
| 390 |
-
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
| 391 |
-
_chat_hist.append((user_message, None)); _status = f"Sending to {model_select} via {provider_select}..."
|
| 392 |
-
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
| 393 |
-
|
| 394 |
-
# Pass the API key from the UI directly to model_logic
|
| 395 |
-
api_key_override = api_key_input
|
| 396 |
-
# model_id = get_model_id_from_display_name(provider_select, model_select) # model_logic handles display name to ID
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
|
| 400 |
-
|
| 401 |
-
# Include current file contents in the prompt as context for the AI
|
| 402 |
-
# This context is built from the *current cache state* (which was just updated from the formatted markdown)
|
| 403 |
-
current_files_context = ""
|
| 404 |
-
if parsed_code_blocks_state_cache:
|
| 405 |
-
current_files_context = "\n\n## Current Files in Space\n"
|
| 406 |
-
for block in parsed_code_blocks_state_cache:
|
| 407 |
-
if block.get("is_structure_block"):
|
| 408 |
-
current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
|
| 409 |
-
else:
|
| 410 |
-
current_files_context += f"### File: {block['filename']}\n"
|
| 411 |
-
if block.get("is_binary") or block.get("code", "").startswith("["): # Include binary/error markers
|
| 412 |
-
current_files_context += f"{block['code']}\n" # e.g. [Binary file...]
|
| 413 |
-
else:
|
| 414 |
-
current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
|
| 415 |
-
current_files_context += "\n"
|
| 416 |
-
|
| 417 |
-
# Append current file context to the user message
|
| 418 |
-
# This combined message structure helps the model understand the context and the expected output format
|
| 419 |
-
user_message_with_context = user_message.strip()
|
| 420 |
-
if current_files_context.strip():
|
| 421 |
-
user_message_with_context = user_message_with_context + current_files_context + "\nBased on the current files above and our chat history, please provide updated file contents using the `### File: ...\n```...\n```\n` format for any files you are creating, modifying, or want to include in the final output. If you are providing a file structure, use the `## File Structure\n```\n...\n```\n` format. Omit files you want to delete from your response."
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
# Convert history to API messages, including the user message with context
|
| 425 |
-
api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
|
| 426 |
-
|
| 427 |
-
# --- Call the new model_logic streaming function ---
|
| 428 |
try:
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
# Generate stream from model_logic
|
| 436 |
-
for chunk in generate_stream(provider_select, model_select, api_key_override, api_msgs):
|
| 437 |
-
if chunk is None: continue # Skip None chunks if any
|
| 438 |
-
if isinstance(chunk, str) and (chunk.startswith("Error: ") or chunk.startswith("API HTTP Error")):
|
| 439 |
-
# If an error chunk is received, treat it as the final output and stop
|
| 440 |
-
full_bot_response_content = chunk
|
| 441 |
-
error_during_stream = chunk
|
| 442 |
-
break # Stop processing stream
|
| 443 |
-
else:
|
| 444 |
-
# Accumulate response and update the last message in chat_hist
|
| 445 |
-
full_bot_response_content += str(chunk) # Ensure chunk is string
|
| 446 |
-
_chat_hist[-1] = (user_message, full_bot_response_content)
|
| 447 |
-
_status = f"Streaming from {model_select}..."
|
| 448 |
-
# Yield update immediately after receiving chunk
|
| 449 |
-
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
| 450 |
-
|
| 451 |
-
# After the stream finishes or breaks
|
| 452 |
-
if error_during_stream:
|
| 453 |
-
_status = error_during_stream # Set status to the error message
|
| 454 |
-
elif full_bot_response_content and not full_bot_response_content.startswith("Error: "): # Only parse if it's not an error message
|
| 455 |
-
_status = f"Streaming complete. Processing files from {model_select} response..."
|
| 456 |
-
|
| 457 |
-
# Pass the *current state* (updated from markdown at the start)
|
| 458 |
-
# and the *latest bot message content* to the parsing logic.
|
| 459 |
-
# _parse_chat_stream_logic will merge and update based on the bot's response.
|
| 460 |
-
parsing_res = _parse_chat_stream_logic(full_bot_response_content, existing_files_state=parsed_code_blocks_state_cache)
|
| 461 |
-
|
| 462 |
-
if parsing_res["error_message"]:
|
| 463 |
-
_status = f"Parsing Error: {parsing_res['error_message']}"
|
| 464 |
-
# Append parsing error to the bot's response in chat for visibility? Or just status?
|
| 465 |
-
# For now, update status and detected files area with error message
|
| 466 |
-
_detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
|
| 467 |
-
else:
|
| 468 |
-
# Update the global cache with the new state returned by the parser
|
| 469 |
-
parsed_code_blocks_state_cache = parsing_res["parsed_code_blocks"]
|
| 470 |
-
|
| 471 |
-
# Regenerate UI outputs from the *updated* cache
|
| 472 |
-
_formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
| 473 |
-
_status = "Processing complete. Previews updated."
|
| 474 |
else:
|
| 475 |
-
|
| 476 |
-
if
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
def
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
_formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
|
| 524 |
-
_file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
|
| 525 |
-
_build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status will appear here.*", "*Select a file to load or delete.*", "*Space runtime status will appear here after refresh.*"
|
| 526 |
-
_chat_history_clear = [] # Clear chat history on loading a new space
|
| 527 |
-
|
| 528 |
-
# Yield initial state to update UI
|
| 529 |
-
yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, gr.update(value=ui_owner_name), gr.update(value=ui_space_name), _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 530 |
-
|
| 531 |
-
owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
|
| 532 |
-
error_occurred = False
|
| 533 |
-
|
| 534 |
-
if not owner_to_use:
|
| 535 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 536 |
-
if token_err or not token:
|
| 537 |
-
_status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
|
| 538 |
-
else:
|
| 539 |
-
try:
|
| 540 |
-
user_info = build_logic_whoami(token=token)
|
| 541 |
-
if user_info and 'name' in user_info:
|
| 542 |
-
owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
|
| 543 |
-
else:
|
| 544 |
-
_status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
|
| 545 |
-
except Exception as e:
|
| 546 |
-
_status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
|
| 547 |
-
|
| 548 |
-
if not owner_to_use or not ui_space_name:
|
| 549 |
-
if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
|
| 550 |
-
|
| 551 |
-
if error_occurred:
|
| 552 |
-
# Yield error state
|
| 553 |
-
yield (f"*Error: {_status_val}*", f"*Error: {_status_val}*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 554 |
-
parsed_code_blocks_state_cache = [] # Clear cache on error
|
| 555 |
-
return # Stop execution
|
| 556 |
-
|
| 557 |
-
sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
|
| 558 |
-
|
| 559 |
-
# Construct iframe URL early, even if file listing fails
|
| 560 |
-
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' # Fallback owner
|
| 561 |
-
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' # Fallback repo
|
| 562 |
-
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
|
| 563 |
-
_iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
if err_list_files and not file_list:
|
| 567 |
-
_status_val = f"File List Error: {err_list_files}"
|
| 568 |
-
parsed_code_blocks_state_cache = [] # Clear cache on error
|
| 569 |
-
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
| 570 |
-
_file_browser_update = gr.update(visible=True, choices=[], value="Error loading files") # Update file browser with error state
|
| 571 |
-
yield (f"*Error: {err_list_files}*", "*Error loading files*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 572 |
-
return # Stop execution
|
| 573 |
-
|
| 574 |
-
if not file_list:
|
| 575 |
-
_status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
|
| 576 |
-
parsed_code_blocks_state_cache = []
|
| 577 |
-
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
| 578 |
-
_file_browser_update = gr.update(visible=True, choices=[], value="No files found")
|
| 579 |
-
yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 580 |
-
return # Stop execution
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
loaded_files_for_cache = [] # Build a list to become the new cache state
|
| 584 |
-
_status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
|
| 585 |
-
# Yield intermediate status while loading files
|
| 586 |
-
yield (_formatted_md_val, _detected_preview_val, _status_val, gr.update(visible=True, choices=sorted(file_list or []), value=None), updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
for file_path in file_list:
|
| 590 |
-
# Skip files that are likely binary or not user-editable code/text
|
| 591 |
-
# Added more extensions and common non-code files like lock files
|
| 592 |
-
_, ext = os.path.splitext(file_path)
|
| 593 |
-
if ext.lower() in [".png",".jpg",".jpeg",".gif",".ico",".svg",".pt",".bin",".safetensors",".onnx",".woff",".woff2",".ttf",".eot",".zip",".tar",".gz",". هفت",".pdf",".mp4",".avi",".mov",".mp3",".wav",".ogg"] or \
|
| 594 |
-
file_path.startswith(".git") or "/.git/" in file_path or \
|
| 595 |
-
file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
|
| 596 |
-
file_path.endswith(".lock") or \
|
| 597 |
-
file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
|
| 598 |
-
file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
|
| 599 |
-
file_path.startswith("venv/") or "/venv/" in file_path or \
|
| 600 |
-
file_path.startswith(".venv/") or "/.venv/" in file_path or \
|
| 601 |
-
file_path == "README.md" or file_path == "LICENSE": # Optionally skip common non-code files like README/LICENSE
|
| 602 |
-
loaded_files_for_cache.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
|
| 603 |
-
|
| 604 |
-
# Handle potential issues with reading large files or non-utf8 files
|
| 605 |
-
try:
|
| 606 |
-
content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
|
| 607 |
-
if err_get:
|
| 608 |
-
# If there's an error getting content, record it but don't fail the whole load
|
| 609 |
-
loaded_files_for_cache.append({"filename": file_path, "code": f"[Error loading content: {err_get}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
|
| 610 |
-
print(f"Error loading {file_path}: {err_get}");
|
| 611 |
-
continue
|
| 612 |
-
# If content is successfully loaded
|
| 613 |
-
loaded_files_for_cache.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
|
| 614 |
-
except Exception as content_ex:
|
| 615 |
-
# Catch any other unexpected exceptions during file content fetching
|
| 616 |
-
loaded_files_for_cache.append({"filename": file_path, "code": f"[Unexpected error loading content: {content_ex}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
|
| 617 |
-
print(f"Unexpected error loading {file_path}: {content_ex}")
|
| 618 |
-
continue
|
| 619 |
-
|
| 620 |
-
# Add a placeholder structure block if none was loaded (AI will generate one later if needed)
|
| 621 |
-
# This ensures the cache isn't empty except for files
|
| 622 |
-
# structure_block = next((b for b in loaded_files_for_cache if b.get("is_structure_block")), None)
|
| 623 |
-
# if not structure_block:
|
| 624 |
-
# loaded_files_for_cache.insert(0, {"filename": "File Structure (original)", "code": "📁 Root\n ...\n", "language": "plaintext", "is_binary": False, "is_structure_block": True})
|
| 625 |
-
|
| 626 |
-
parsed_code_blocks_state_cache = loaded_files_for_cache
|
| 627 |
-
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
| 628 |
-
_status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready. {len(file_list)} files listed."
|
| 629 |
-
_file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) # Use the full file list for the dropdown
|
| 630 |
-
|
| 631 |
-
# Final yield with updated state
|
| 632 |
-
yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
|
| 636 |
-
# Declare global at the top
|
| 637 |
-
global parsed_code_blocks_state_cache
|
| 638 |
-
# ... (this function calls build_logic_create_space and refreshes file list)
|
| 639 |
-
_build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
|
| 640 |
-
# Include outputs for owner/space name textboxes in the initial yield
|
| 641 |
-
yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part) # Yield initial status
|
| 642 |
-
if not ui_space_name_part or "/" in ui_space_name_part: _build_status = f"Build Error: HF Space Name '{ui_space_name_part}' must be repo name only (no '/')."; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
|
| 643 |
-
final_owner_for_build = ui_owner_name_part
|
| 644 |
-
if not final_owner_for_build:
|
| 645 |
-
token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 646 |
-
if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
|
| 647 |
-
if token_for_whoami:
|
| 648 |
-
try:
|
| 649 |
-
user_info = build_logic_whoami(token=token_for_whoami)
|
| 650 |
-
final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
|
| 651 |
-
if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
|
| 652 |
-
except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
|
| 653 |
-
else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
|
| 654 |
-
|
| 655 |
-
if not final_owner_for_build: _build_status = "Build Error: HF Owner Name could not be determined. Please specify it."; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
|
| 656 |
-
|
| 657 |
-
# Before building, parse the markdown to ensure the cache reflects exactly what's being built
|
| 658 |
-
# This prevents inconsistencies if the user manually edited the markdown output
|
| 659 |
try:
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
| 690 |
-
|
| 691 |
-
except Exception as e:
|
| 692 |
-
_build_status = f"Build Error: Failed to parse Markdown structure before building: {e}";
|
| 693 |
-
# Yield error status, including keeping current owner/space name in textboxes
|
| 694 |
-
yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part); return # Stop build on parse error
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
|
| 698 |
-
_build_status = f"Build Process: {result_message}"
|
| 699 |
-
|
| 700 |
-
# Update UI with owner/space names extracted from markdown if present
|
| 701 |
-
owner_name_output = gr.update(value=ui_owner_name_part)
|
| 702 |
-
space_name_output = gr.update(value=ui_space_name_part)
|
| 703 |
-
|
| 704 |
-
if "Successfully" in result_message:
|
| 705 |
-
# Use potentially updated owner/space name from markdown parsing
|
| 706 |
-
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name_part.lower()).strip('-') or 'owner'
|
| 707 |
-
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
|
| 708 |
-
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
|
| 709 |
-
_iframe_html = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
|
| 710 |
-
_build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{ui_owner_name_part}/{ui_space_name_part})"
|
| 711 |
-
|
| 712 |
-
# Refresh file list after successful build
|
| 713 |
-
file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
|
| 714 |
-
if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value="Error refreshing files")
|
| 715 |
-
else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
|
| 716 |
-
|
| 717 |
-
# Final yield including potential updates to owner/space name textboxes
|
| 718 |
-
yield _build_status, _iframe_html, _file_browser_update, owner_name_output, space_name_output
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
# File editing handlers are okay, just need to ensure they update the cache properly after commit/delete
|
| 722 |
-
def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
|
| 723 |
-
# Declare global at the top (even though it's not modified here, it's good practice if the function *might* interact with it in the future, or just for consistency)
|
| 724 |
-
# In this specific function, `global` isn't strictly needed as it only *reads* indirectly via _generate_ui_outputs_from_cache which handles its own global
|
| 725 |
-
# Keeping it here for consistency as per the error symptoms observed in similar functions.
|
| 726 |
-
global parsed_code_blocks_state_cache # Added global here
|
| 727 |
-
|
| 728 |
-
_file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="python") # Reset values
|
| 729 |
-
if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
| 730 |
-
yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update # Clear editor and status
|
| 731 |
-
return
|
| 732 |
-
|
| 733 |
-
owner_to_use = ui_owner_name_part
|
| 734 |
-
if not owner_to_use:
|
| 735 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 736 |
-
if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
| 737 |
-
if token:
|
| 738 |
-
try:
|
| 739 |
-
user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
|
| 740 |
-
if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
| 741 |
-
except Exception as e: _edit_status_val = f"Error auto-detecting owner for editing file: {e}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
| 742 |
-
else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
| 743 |
-
|
| 744 |
-
if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
| 745 |
-
|
| 746 |
-
_edit_status_val = f"Loading {selected_file_path}..."
|
| 747 |
-
yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="python") # Yield loading state
|
| 748 |
-
|
| 749 |
-
content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
|
| 750 |
-
|
| 751 |
-
if err:
|
| 752 |
-
_edit_status_val = f"Error loading '{selected_file_path}': {err}"
|
| 753 |
-
_commit_msg_val = f"Error loading {selected_file_path}"
|
| 754 |
-
_file_content_val = f"Error loading {selected_file_path}:\n{err}"
|
| 755 |
-
_lang_update = gr.update(language="python") # Default language for error display
|
| 756 |
-
yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
|
| 757 |
-
return
|
| 758 |
-
|
| 759 |
-
_file_content_val = content or ""
|
| 760 |
-
_edit_status_val = f"Loaded {selected_file_path} for editing."
|
| 761 |
-
_commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
|
| 762 |
-
_lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
|
| 763 |
-
|
| 764 |
-
yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
|
| 765 |
-
|
| 766 |
-
def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_edit_path, edited_content, commit_message):
|
| 767 |
-
# Declare global at the top
|
| 768 |
-
global parsed_code_blocks_state_cache
|
| 769 |
-
_edit_status_val = "Processing commit..."
|
| 770 |
-
# Initialize updates for components that might change
|
| 771 |
-
_file_browser_update_val = gr.update() # Will update choices or value
|
| 772 |
-
_formatted_md_out = gr.update() # Will update markdown
|
| 773 |
-
_detected_preview_out = gr.update() # Will update markdown preview
|
| 774 |
-
_download_btn_out = gr.update() # Will update download button
|
| 775 |
-
|
| 776 |
-
yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out # Yield initial status
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
| 780 |
-
_edit_status_val = "Error: No valid file selected for commit.";
|
| 781 |
-
yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
|
| 782 |
-
|
| 783 |
-
owner_to_use = ui_owner_name_part
|
| 784 |
-
if not owner_to_use:
|
| 785 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 786 |
-
if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 787 |
-
if token:
|
| 788 |
-
try:
|
| 789 |
-
user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
|
| 790 |
-
if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 791 |
-
except Exception as e: _edit_status_val = f"Error auto-detecting owner for committing file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 792 |
-
else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 793 |
-
|
| 794 |
-
if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 795 |
-
|
| 796 |
-
status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
|
| 797 |
-
_edit_status_val = status_msg
|
| 798 |
-
|
| 799 |
-
if "Successfully updated" in status_msg:
|
| 800 |
-
# Update the cache with the new content
|
| 801 |
-
found_in_cache = False
|
| 802 |
-
for block in parsed_code_blocks_state_cache:
|
| 803 |
-
if block["filename"] == file_to_edit_path:
|
| 804 |
-
block["code"] = edited_content
|
| 805 |
-
block["language"] = _infer_lang_from_filename(file_to_edit_path)
|
| 806 |
-
block["is_binary"] = False # Assume user edited text content
|
| 807 |
-
block["is_structure_block"] = False # Ensure it's not marked as structure
|
| 808 |
-
found_in_cache = True
|
| 809 |
-
break
|
| 810 |
-
if not found_in_cache:
|
| 811 |
-
# If file was added/edited via editor and wasn't in initial load cache (e.g. binary/error placeholder), add/replace it
|
| 812 |
-
# First remove any existing placeholder for this file
|
| 813 |
-
parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_edit_path]
|
| 814 |
-
# Then add the new text content block
|
| 815 |
-
parsed_code_blocks_state_cache.append({
|
| 816 |
-
"filename": file_to_edit_path,
|
| 817 |
-
"code": edited_content,
|
| 818 |
-
"language": _infer_lang_from_filename(file_to_edit_path),
|
| 819 |
-
"is_binary": False,
|
| 820 |
-
"is_structure_block": False
|
| 821 |
-
})
|
| 822 |
-
# Re-sort the cache to maintain consistent order
|
| 823 |
-
parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
| 824 |
-
|
| 825 |
-
# Regenerate markdown and preview from the updated cache
|
| 826 |
-
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
| 827 |
-
|
| 828 |
-
# Refresh file list choices and keep the current file selected
|
| 829 |
-
new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
| 830 |
-
if err_list:
|
| 831 |
-
_edit_status_val += f"\nFile list refresh error: {err_list}"
|
| 832 |
-
_file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
|
| 833 |
-
else:
|
| 834 |
-
_file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path) # Keep current file selected
|
| 835 |
-
|
| 836 |
-
yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
|
| 840 |
-
# Declare global at the top
|
| 841 |
-
global parsed_code_blocks_state_cache
|
| 842 |
-
_edit_status_val = "Processing deletion..."
|
| 843 |
-
# Initialize updates for components that might change/clear
|
| 844 |
-
_file_browser_choices_update = gr.update() # Update choices
|
| 845 |
-
_file_browser_value_update = None # Clear selected file value
|
| 846 |
-
_file_content_editor_update = gr.update(value="") # Clear editor content
|
| 847 |
-
_commit_msg_update = gr.update(value="") # Clear commit message
|
| 848 |
-
_lang_update = gr.update(language="plaintext") # Reset editor language
|
| 849 |
-
_formatted_md_out = gr.update() # Update markdown
|
| 850 |
-
_detected_preview_out = gr.update() # Update markdown preview
|
| 851 |
-
_download_btn_out = gr.update() # Update download button
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out) # Yield initial status
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
| 858 |
-
_edit_status_val = "Error: No valid file selected for deletion.";
|
| 859 |
-
yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 860 |
-
|
| 861 |
-
owner_to_use = ui_owner_name_part
|
| 862 |
-
if not owner_to_use:
|
| 863 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 864 |
-
if token_err: _edit_status_val = f"API Token Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 865 |
-
if token:
|
| 866 |
-
try:
|
| 867 |
-
user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
|
| 868 |
-
if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 869 |
-
except Exception as e: _edit_status_val = f"Error auto-detecting owner for deleting file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 870 |
-
else: _edit_status_val = "Error: HF Token needed to auto-detect owner."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: Owner and Space Name are required."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
| 874 |
-
|
| 875 |
-
deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
|
| 876 |
-
_edit_status_val = deletion_status_msg
|
| 877 |
-
|
| 878 |
-
# Always refresh the file list dropdown choices after a delete attempt, successful or not
|
| 879 |
-
new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
| 880 |
-
|
| 881 |
-
if "Successfully deleted" in deletion_status_msg:
|
| 882 |
-
# Remove the file from the cache
|
| 883 |
-
parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
|
| 884 |
-
|
| 885 |
-
# Regenerate markdown and preview from the updated cache
|
| 886 |
-
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
if err_list:
|
| 890 |
-
_edit_status_val += f"\nFile list refresh error: {err_list}"
|
| 891 |
-
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files") # Set value to error state
|
| 892 |
-
else:
|
| 893 |
-
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None) # Clear selection visually and internally
|
| 894 |
-
|
| 895 |
-
_file_browser_value_update = None # Explicitly set value to None to clear selection visual
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
else: # If deletion failed
|
| 899 |
-
if err_list:
|
| 900 |
-
_edit_status_val += f"\nFile list refresh error: {err_list}"
|
| 901 |
-
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
|
| 902 |
-
_file_browser_value_update = "Error refreshing files" # Keep error state in value if list failed
|
| 903 |
-
else:
|
| 904 |
-
# If list refresh succeeded but delete failed, refresh choices and keep the *failed-to-delete* file selected
|
| 905 |
-
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
|
| 906 |
-
_file_browser_value_update = file_to_delete_path # Keep the file selected visually
|
| 907 |
-
|
| 908 |
-
# Markdown and preview are not changed if deletion failed, keep current updates as gr.update()
|
| 909 |
-
# Regenerate previews to show they are unchanged
|
| 910 |
-
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out)
|
| 914 |
-
|
| 915 |
-
|
| 916 |
-
# Space status handler is okay
|
| 917 |
-
def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
|
| 918 |
-
# This function doesn't modify the global cache, so no global declaration needed.
|
| 919 |
-
# ... (rest of this function is the same)
|
| 920 |
-
yield "*Fetching space status...*" # Initial feedback
|
| 921 |
-
owner_to_use = ui_owner_name
|
| 922 |
-
if not owner_to_use:
|
| 923 |
-
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
| 924 |
-
if token_err or not token: yield f"**Error:** {token_err or 'Cannot determine owner.'}"; return
|
| 925 |
-
try: user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
|
| 926 |
-
except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
|
| 927 |
-
if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
|
| 928 |
-
status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
|
| 929 |
-
if error_msg: _status_display_md = f"**Error fetching status for {owner_to_use}/{ui_space_name}:**\n\n`{escape_html_for_markdown(error_msg)}`"
|
| 930 |
-
elif status_details:
|
| 931 |
-
stage, hardware, error, log_link = status_details.get('stage','N/A'), status_details.get('hardware','N/A'), status_details.get('error_message'), status_details.get('full_log_link','#')
|
| 932 |
-
md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
|
| 933 |
-
if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
|
| 934 |
-
if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
|
| 935 |
-
md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
|
| 936 |
-
if status_details.get('raw_data'):
|
| 937 |
-
# Add raw data in a collapsible section for debugging
|
| 938 |
-
md_lines.append(f"\n<details><summary>Raw Status Data (JSON)</summary>\n\n```json\n{json.dumps(status_details.get('raw_data', {}), indent=2)}\n```\n</details>")
|
| 939 |
-
|
| 940 |
-
_status_display_md = "\n".join(md_lines)
|
| 941 |
-
else: _status_display_md = "Could not retrieve status details."
|
| 942 |
-
yield _status_display_md
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
# Define a custom theme with a dark background and contrasting colors
|
| 946 |
-
# And add custom CSS for a background gradient and component styling
|
| 947 |
-
custom_theme = gr.themes.Base(
|
| 948 |
-
primary_hue="teal", # Teal for primary actions
|
| 949 |
-
secondary_hue="purple", # Purple for secondary elements
|
| 950 |
-
neutral_hue="zinc", # Zinc for neutral/backgrounds (dark gray)
|
| 951 |
-
text_size="sm", # Smaller text size for a denser, professional look
|
| 952 |
-
spacing_size="md", # Medium spacing
|
| 953 |
-
radius_size="sm", # Small border radius
|
| 954 |
-
font=["System UI", "sans-serif"] # Use system font
|
| 955 |
-
)
|
| 956 |
-
|
| 957 |
-
custom_css = """
|
| 958 |
-
body {
|
| 959 |
-
background: linear-gradient(to bottom right, #2c3e50, #34495e); /* Dark blue-gray gradient */
|
| 960 |
-
color: #ecf0f1; /* Light text color for dark background */
|
| 961 |
-
}
|
| 962 |
-
/* Adjust main Gradio container background to be transparent to see body gradient */
|
| 963 |
-
.gradio-container {
|
| 964 |
-
background: transparent !important;
|
| 965 |
-
}
|
| 966 |
-
/* Adjust component backgrounds for contrast against the body gradient */
|
| 967 |
-
.gr-box, .gr-panel, .gr-pill {
|
| 968 |
-
background-color: rgba(44, 62, 80, 0.8) !important; /* Slightly lighter transparent dark blue-gray */
|
| 969 |
-
border-color: rgba(189, 195, 199, 0.2) !important; /* Light border for contrast */
|
| 970 |
-
}
|
| 971 |
-
/* Adjust inputs, dropdowns, buttons etc. for visibility */
|
| 972 |
-
.gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message {
|
| 973 |
-
border-color: rgba(189, 195, 199, 0.3) !important;
|
| 974 |
-
background-color: rgba(52, 73, 94, 0.9) !important; /* Slightly different dark blue-gray */
|
| 975 |
-
color: #ecf0f1 !important; /* Ensure text is light */
|
| 976 |
-
}
|
| 977 |
-
.gr-button.gr-button-primary {
|
| 978 |
-
background-color: #1abc9c !important; /* Teal from primary_hue */
|
| 979 |
-
color: white !important;
|
| 980 |
-
border-color: #16a085 !important;
|
| 981 |
-
}
|
| 982 |
-
.gr-button.gr-button-secondary {
|
| 983 |
-
background-color: #9b59b6 !important; /* Purple from secondary_hue */
|
| 984 |
-
color: white !important;
|
| 985 |
-
border-color: #8e44ad !important;
|
| 986 |
-
}
|
| 987 |
-
.gr-button.gr-button-stop {
|
| 988 |
-
background-color: #e74c3c !important; /* Red for stop/delete */
|
| 989 |
-
color: white !important;
|
| 990 |
-
border-color: #c0392b !important;
|
| 991 |
-
}
|
| 992 |
-
/* Adjust markdown backgrounds */
|
| 993 |
-
.gr-markdown {
|
| 994 |
-
background-color: rgba(44, 62, 80, 0.7) !important; /* Transparent dark background */
|
| 995 |
-
padding: 10px; /* Add some padding */
|
| 996 |
-
border-radius: 5 al;
|
| 997 |
-
}
|
| 998 |
-
/* Style markdown headers for better contrast */
|
| 999 |
-
.gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
|
| 1000 |
-
color: #ecf0f1 !important; /* Ensure headers are light */
|
| 1001 |
-
border-bottom-color: rgba(189, 195, 199, 0.3) !important; /* Light separator */
|
| 1002 |
-
}
|
| 1003 |
-
/* Style code blocks within markdown */
|
| 1004 |
-
.gr-markdown pre code {
|
| 1005 |
-
background-color: rgba(52, 73, 94, 0.95) !important; /* Darker code background */
|
| 1006 |
-
border-color: rgba(189, 195, 199, 0.3) !important;
|
| 1007 |
-
}
|
| 1008 |
-
/* Chatbot specific styling */
|
| 1009 |
-
.gr-chatbot {
|
| 1010 |
-
background-color: rgba(44, 62, 80, 0.7) !important;
|
| 1011 |
-
border-color: rgba(189, 195, 199, 0.2) !important;
|
| 1012 |
-
}
|
| 1013 |
-
.gr-chatbot .message {
|
| 1014 |
-
background-color: rgba(52, 73, 94, 0.9) !important; /* Dark background for messages */
|
| 1015 |
-
color: #ecf0f1 !important;
|
| 1016 |
-
border-color: rgba(189, 195, 199, 0.3) !important;
|
| 1017 |
-
}
|
| 1018 |
-
.gr-chatbot .message.user {
|
| 1019 |
-
background-color: rgba(46, 204, 113, 0.9) !important; /* Greenish background for user messages */
|
| 1020 |
-
color: black !important; /* Dark text for green background */
|
| 1021 |
-
}
|
| 1022 |
-
"""
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
# Get initial providers and models for UI setup
|
| 1026 |
-
available_providers = get_available_providers()
|
| 1027 |
-
default_provider = available_providers[0] if available_providers else None
|
| 1028 |
-
initial_models = get_models_for_provider(default_provider) if default_provider else []
|
| 1029 |
-
initial_default_model = get_default_model_for_provider(default_provider) if default_provider else None
|
| 1030 |
-
# Ensure initial_default_model is in the initial_models list, fallback if not
|
| 1031 |
-
if initial_default_model not in initial_models and initial_models:
|
| 1032 |
-
initial_default_model = initial_models[0]
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
|
| 1036 |
-
gr.Markdown("# 🤖 AI Code & Space Generator")
|
| 1037 |
-
gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
|
| 1038 |
-
with gr.Row():
|
| 1039 |
-
with gr.Sidebar():
|
| 1040 |
-
gr.Markdown("## ⚙️ Configuration")
|
| 1041 |
-
with gr.Group(): gr.Markdown("### API Keys & Tokens");
|
| 1042 |
-
# Single API key input, model_logic decides which env var to check or uses this override
|
| 1043 |
-
api_key_input = gr.Textbox(label="AI Provider API Key (Optional Override)", type="password", placeholder="Paste key here or set env var (e.g., GROQ_API_KEY)");
|
| 1044 |
-
hf_api_key_input = gr.Textbox(label="Hugging Face Token (for building/loading)", type="password", placeholder="hf_...")
|
| 1045 |
-
with gr.Group(): gr.Markdown("### Hugging Face Space"); owner_name_input = gr.Textbox(label="HF Owner Name", placeholder="e.g., your-username"); space_name_input = gr.Textbox(label="HF Space Name", value="my-ai-space", placeholder="e.g., my-cool-app"); space_sdk_select = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio", info="Used for new/build."); load_space_button = gr.Button("🔄 Load Existing Space", variant="secondary", size="sm")
|
| 1046 |
-
with gr.Group(): gr.Markdown("### AI Model Settings");
|
| 1047 |
-
provider_select = gr.Dropdown(label="AI Provider", choices=available_providers, value=default_provider, info="Select an AI model provider.");
|
| 1048 |
-
model_select = gr.Dropdown(label="AI Model", choices=initial_models, value=initial_default_model, info="Select a model.");
|
| 1049 |
-
system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
|
| 1050 |
-
with gr.Column(scale=3):
|
| 1051 |
-
gr.Markdown("## 💬 AI Chat & Code Generation")
|
| 1052 |
-
# Updated chatbot avatar
|
| 1053 |
-
chatbot_display = gr.Chatbot(label="AI Chat", height=400, bubble_full_width=False, avatar_images=(None, "https://huggingface.co/datasets/huggingface/badges/resolve/main/huggingface-bot-avatar.svg"))
|
| 1054 |
-
with gr.Row(): chat_message_input = gr.Textbox(show_label=False, placeholder="Your Message...", scale=7); send_chat_button = gr.Button("Send", variant="primary", scale=1, size="lg")
|
| 1055 |
-
status_output = gr.Textbox(label="Chat/Process Status", interactive=False, lines=1, value="Ready.")
|
| 1056 |
-
gr.Markdown("---")
|
| 1057 |
-
with gr.Tabs():
|
| 1058 |
-
with gr.TabItem("📝 Formatted Space Markdown"): gr.Markdown("Complete Markdown definition for your Space."); formatted_space_output_display = gr.Textbox(label="Current Space Definition", lines=15, interactive=True, show_copy_button=True, value="*Space definition...*"); download_button = gr.DownloadButton(label="Download .md", interactive=False, size="sm")
|
| 1059 |
-
with gr.TabItem("🔍 Detected Files Preview"):
|
| 1060 |
-
detected_files_preview = gr.Markdown(value="*Files preview...*")
|
| 1061 |
-
|
| 1062 |
-
gr.Markdown("---")
|
| 1063 |
-
with gr.Tabs():
|
| 1064 |
-
with gr.TabItem("🚀 Build & Preview Space"):
|
| 1065 |
-
with gr.Row(): build_space_button = gr.Button("Build / Update Space on HF", variant="primary", scale=2); refresh_status_button = gr.Button("🔄 Refresh Space Status", scale=1)
|
| 1066 |
-
# Build status outputs also include updating owner/space names in the textboxes
|
| 1067 |
-
build_status_display = gr.Textbox(label="Build Operation Status", interactive=False, lines=2, value="*Build status will appear here.*"); gr.Markdown("---"); space_runtime_status_display = gr.Markdown("*Space runtime status will appear here after refresh.*"); gr.Markdown("---"); space_iframe_display = gr.HTML(value="<!-- Space Iframe -->", visible=False)
|
| 1068 |
-
with gr.TabItem("✏️ Edit Space Files"):
|
| 1069 |
-
gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
|
| 1070 |
-
file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
|
| 1071 |
-
file_content_editor = gr.Code(label="File Content Editor", language="python", lines=15, interactive=True)
|
| 1072 |
-
commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Updated app.py", value="Update via AI Space Editor")
|
| 1073 |
-
with gr.Row(): update_file_button = gr.Button("Commit Changes", variant="primary", scale=2); delete_file_button = gr.Button("🗑️ Delete Selected File", variant="stop", scale=1)
|
| 1074 |
-
edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
|
| 1075 |
-
|
| 1076 |
-
# --- Event Handlers ---
|
| 1077 |
-
|
| 1078 |
-
# Provider dropdown change event to update model dropdown
|
| 1079 |
-
provider_select.change(
|
| 1080 |
-
fn=update_models_dropdown,
|
| 1081 |
-
inputs=provider_select,
|
| 1082 |
-
outputs=model_select
|
| 1083 |
-
)
|
| 1084 |
-
|
| 1085 |
-
# Chat submit handler outputs
|
| 1086 |
-
chat_outputs = [chat_message_input, chatbot_display, status_output, detected_files_preview, formatted_space_output_display, download_button]
|
| 1087 |
-
# Chat submit handler inputs
|
| 1088 |
-
chat_inputs = [chat_message_input, chatbot_display, api_key_input, provider_select, model_select, system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display] # Pass current formatted markdown as context
|
| 1089 |
-
|
| 1090 |
-
# Wire chat buttons
|
| 1091 |
-
send_chat_button.click(
|
| 1092 |
-
fn=handle_chat_submit,
|
| 1093 |
-
inputs=chat_inputs,
|
| 1094 |
-
outputs=chat_outputs
|
| 1095 |
)
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
|
| 1126 |
if __name__ == "__main__":
|
| 1127 |
-
|
|
|
|
| 1 |
+
# keylock/app.py
|
| 2 |
import gradio as gr
|
| 3 |
+
from PIL import Image, ImageFont
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import tempfile
|
| 5 |
+
import os
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
import traceback
|
| 9 |
+
import base64
|
| 10 |
+
import io
|
| 11 |
+
|
| 12 |
+
from . import core # Use relative import for core module
|
| 13 |
+
from . import __version__ # Import version for footer
|
| 14 |
+
|
| 15 |
+
app_logger = logging.getLogger("keylock_app")
|
| 16 |
+
if not app_logger.hasHandlers(): # Basic logging setup if not configured elsewhere
|
| 17 |
+
handler = logging.StreamHandler()
|
| 18 |
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
| 19 |
+
handler.setFormatter(formatter)
|
| 20 |
+
app_logger.addHandler(handler)
|
| 21 |
+
app_logger.setLevel(logging.INFO)
|
| 22 |
+
|
| 23 |
+
# Theming
|
| 24 |
+
try:
|
| 25 |
+
font_family = [gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"]
|
| 26 |
+
except AttributeError:
|
| 27 |
+
app_logger.warning("gr.themes.GoogleFont not found. Using fallback fonts. This might be due to Gradio version.")
|
| 28 |
+
font_family = ["ui-sans-serif", "system-ui", "sans-serif"]
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
blue_color = gr.themes.colors.blue
|
| 32 |
+
sky_color = gr.themes.colors.sky
|
| 33 |
+
slate_color = gr.themes.colors.slate
|
| 34 |
+
cyan_color = gr.themes.colors.cyan
|
| 35 |
+
neutral_color = gr.themes.colors.neutral
|
| 36 |
+
except AttributeError:
|
| 37 |
+
app_logger.warning("gr.themes.colors not found. Using placeholder colors for themes. This might be due to Gradio version.")
|
| 38 |
+
class FallbackColors: # Basic fallback colors
|
| 39 |
+
blue = "blue"; sky = "skyblue"; slate = "slategray"; cyan = "cyan"; neutral = "gray"
|
| 40 |
+
blue_color = FallbackColors.blue
|
| 41 |
+
sky_color = FallbackColors.sky
|
| 42 |
+
slate_color = FallbackColors.slate
|
| 43 |
+
cyan_color = FallbackColors.cyan
|
| 44 |
+
neutral_color = FallbackColors.neutral
|
| 45 |
+
|
| 46 |
+
ICON_EMBED = "➕"
|
| 47 |
+
ICON_EXTRACT = "➖"
|
| 48 |
+
|
| 49 |
+
def pil_to_base64_html(pil_image, max_width_px=None):
|
| 50 |
+
buffered = io.BytesIO(); pil_image.save(buffered, format="PNG")
|
| 51 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
| 52 |
+
style = f"max-width:{max_width_px}px; height:auto; border:1px solid #ccc; display:block; margin-left:auto; margin-right:auto;" if max_width_px else "border:1px solid #ccc; display:block; margin-left:auto; margin-right:auto;"
|
| 53 |
+
return f"<div style='text-align:center;'><img src='data:image/png;base64,{img_str}' alt='Stego Image' style='{style}'/></div>"
|
| 54 |
+
|
| 55 |
+
def gradio_embed_data(kv_string: str, password: str,
|
| 56 |
+
input_image_pil: Image.Image, generate_carrier_flag: bool,
|
| 57 |
+
show_keys_on_image_flag: bool, output_filename_base: str):
|
| 58 |
+
output_html_img_str, status_msg, dl_file_path = None, "An error occurred.", None
|
| 59 |
+
if not password: return None, "Error: Password cannot be empty.", None
|
| 60 |
+
if not kv_string or not kv_string.strip(): return None, "Error: Key-Value data cannot be empty.", None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
try:
|
| 62 |
+
data_dict = core.parse_kv_string_to_dict(kv_string)
|
| 63 |
+
if not data_dict: return None, "Error: Parsed Key-Value data is empty.", None
|
| 64 |
+
|
| 65 |
+
original_format_note = ""
|
| 66 |
+
if generate_carrier_flag or input_image_pil is None:
|
| 67 |
+
carrier_img = core.generate_keylock_carrier_image()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
else:
|
| 69 |
+
carrier_img = input_image_pil.copy()
|
| 70 |
+
if hasattr(input_image_pil, 'format') and input_image_pil.format and input_image_pil.format.upper() != 'PNG':
|
| 71 |
+
original_format_note = (
|
| 72 |
+
f"Input carrier image was format '{input_image_pil.format}'. "
|
| 73 |
+
f"It will be processed and saved as PNG. "
|
| 74 |
+
)
|
| 75 |
+
app_logger.warning(
|
| 76 |
+
f"{original_format_note}If original was lossy (e.g., JPEG), quality is preserved from upload; "
|
| 77 |
+
f"if it had transparency (e.g., GIF), it will be lost during RGB conversion."
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
carrier_img = carrier_img.convert("RGB")
|
| 81 |
+
|
| 82 |
+
keys_for_overlay = list(data_dict.keys()) if show_keys_on_image_flag else None
|
| 83 |
+
overlay_title = "KeyLock: Data Embedded"
|
| 84 |
+
|
| 85 |
+
final_carrier_with_overlay = core.draw_key_list_dropdown_overlay(
|
| 86 |
+
carrier_img,
|
| 87 |
+
keys=keys_for_overlay,
|
| 88 |
+
title=overlay_title
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
serial_data = json.dumps(data_dict).encode('utf-8')
|
| 92 |
+
encrypted_data = core.encrypt_data(serial_data, password)
|
| 93 |
+
|
| 94 |
+
stego_final_img = core.embed_data_in_image(final_carrier_with_overlay, encrypted_data)
|
| 95 |
+
stego_final_img = core.set_pil_image_format_to_png(stego_final_img)
|
| 96 |
+
|
| 97 |
+
fname_base = "".join(c if c.isalnum() or c in ('_','-') else '_' for c in output_filename_base.strip()) or "keylock_img"
|
| 98 |
+
temp_fp = None
|
| 99 |
+
with tempfile.NamedTemporaryFile(prefix=fname_base+"_", suffix=".png", delete=False) as tmp:
|
| 100 |
+
stego_final_img.save(tmp, format="PNG")
|
| 101 |
+
temp_fp = tmp.name
|
| 102 |
+
|
| 103 |
+
output_html_img_str = pil_to_base64_html(stego_final_img, max_width_px=480)
|
| 104 |
+
status_msg = (f"Data embedded into '{os.path.basename(temp_fp)}'.\n"
|
| 105 |
+
f"{original_format_note}"
|
| 106 |
+
f"Image contains visual \"{overlay_title}\" overlay "
|
| 107 |
+
f"{'(with key list)' if show_keys_on_image_flag and keys_for_overlay else ''} "
|
| 108 |
+
f"and your LSB-encoded secret data.\n"
|
| 109 |
+
f"Secrets: {len(serial_data)}B (raw), {len(encrypted_data)}B (encrypted).")
|
| 110 |
+
return output_html_img_str, status_msg, temp_fp
|
| 111 |
+
except ValueError as e: return None, f"Error: {str(e)}", None
|
| 112 |
+
except Exception as e: app_logger.error(f"Embed Error: {e}", exc_info=True); return None, f"Unexpected Error: {str(e)}", None
|
| 113 |
+
|
| 114 |
+
def gradio_extract_data(stego_image_pil: Image.Image, password: str):
|
| 115 |
+
if stego_image_pil is None: return "Error: No image provided.", "Error: No image."
|
| 116 |
+
if not password: return "Error: Password cannot be empty.", "Error: Password required."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
try:
|
| 118 |
+
stego_image_rgb = stego_image_pil.convert("RGB")
|
| 119 |
+
if hasattr(stego_image_pil, 'format') and stego_image_pil.format and stego_image_pil.format.upper() != "PNG":
|
| 120 |
+
app_logger.warning(f"Uploaded image for extraction is format '{stego_image_pil.format}', not PNG. LSB data may be compromised if not the original KeyLock file.")
|
| 121 |
+
|
| 122 |
+
extracted_data = core.extract_data_from_image(stego_image_rgb)
|
| 123 |
+
decrypted_bytes = core.decrypt_data(extracted_data, password)
|
| 124 |
+
try:
|
| 125 |
+
data = json.loads(decrypted_bytes.decode('utf-8'))
|
| 126 |
+
txt, stat = json.dumps(data, indent=2), "Data extracted successfully (JSON)."
|
| 127 |
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
| 128 |
+
try:
|
| 129 |
+
txt = "Decrypted (UTF-8, not JSON):\n"+decrypted_bytes.decode('utf-8')
|
| 130 |
+
stat = "Warning: Decrypted as UTF-8 (not JSON)."
|
| 131 |
+
except UnicodeDecodeError:
|
| 132 |
+
txt = "Decrypted (raw hex, not JSON/UTF-8):\n"+decrypted_bytes.hex()
|
| 133 |
+
stat = "Warning: Decrypted as raw hex."
|
| 134 |
+
return txt, stat
|
| 135 |
+
except ValueError as e: return f"Error: {str(e)}", f"Extraction Failed: {str(e)}"
|
| 136 |
+
except Exception as e: app_logger.error(f"Extract Error: {e}", exc_info=True); return f"Unexpected Error: {str(e)}", f"Error: {str(e)}"
|
| 137 |
+
|
| 138 |
+
def build_interface():
|
| 139 |
+
custom_theme = gr.themes.Base(
|
| 140 |
+
primary_hue="teal",
|
| 141 |
+
secondary_hue="purple",
|
| 142 |
+
neutral_hue="zinc",
|
| 143 |
+
text_size="sm",
|
| 144 |
+
spacing_size="md",
|
| 145 |
+
radius_size="sm",
|
| 146 |
+
font=["System UI", "sans-serif"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
)
|
| 148 |
+
custom_css = """
|
| 149 |
+
body {
|
| 150 |
+
background: linear-gradient(to bottom right, #2c3e50, #34495e);
|
| 151 |
+
color: #ecf0f1;
|
| 152 |
+
}
|
| 153 |
+
.gradio-container {
|
| 154 |
+
background: transparent !important;
|
| 155 |
+
}
|
| 156 |
+
.gr-box, .gr-panel, .gr-pill {
|
| 157 |
+
background-color: rgba(44, 62, 80, 0.8) !important;
|
| 158 |
+
border-color: rgba(189, 195, 199, 0.2) !important;
|
| 159 |
+
}
|
| 160 |
+
.gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message, .gr-image {
|
| 161 |
+
border-color: rgba(189, 195, 199, 0.3) !important;
|
| 162 |
+
background-color: rgba(52, 73, 94, 0.9) !important;
|
| 163 |
+
color: #ecf0f1 !important;
|
| 164 |
+
}
|
| 165 |
+
.gr-button.gr-button-primary {
|
| 166 |
+
background-color: #1abc9c !important;
|
| 167 |
+
color: white !important;
|
| 168 |
+
border-color: #16a085 !important;
|
| 169 |
+
}
|
| 170 |
+
.gr-button.gr-button-secondary {
|
| 171 |
+
background-color: #9b59b6 !important;
|
| 172 |
+
color: white !important;
|
| 173 |
+
border-color: #8e44ad !important;
|
| 174 |
+
}
|
| 175 |
+
.gr-button.gr-button-stop {
|
| 176 |
+
background-color: #e74c3c !important;
|
| 177 |
+
color: white !important;
|
| 178 |
+
border-color: #c0392b !important;
|
| 179 |
+
}
|
| 180 |
+
.gr-markdown {
|
| 181 |
+
background-color: rgba(44, 62, 80, 0.7) !important;
|
| 182 |
+
padding: 10px;
|
| 183 |
+
border-radius: 5px;
|
| 184 |
+
}
|
| 185 |
+
.gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
|
| 186 |
+
color: #ecf0f1 !important;
|
| 187 |
+
border-bottom-color: rgba(189, 195, 199, 0.3) !important;
|
| 188 |
+
}
|
| 189 |
+
.gr-markdown pre code {
|
| 190 |
+
background-color: rgba(52, 73, 94, 0.95) !important;
|
| 191 |
+
border-color: rgba(189, 195, 199, 0.3) !important;
|
| 192 |
+
}
|
| 193 |
+
.gr-image div img { /* Style for image preview */
|
| 194 |
+
border: 1px solid #ccc;
|
| 195 |
+
background-color: rgba(52, 73, 94, 0.9) !important;
|
| 196 |
+
}
|
| 197 |
+
.gr-file div button { /* Style for file download button */
|
| 198 |
+
background-color: #1abc9c !important;
|
| 199 |
+
color: white !important;
|
| 200 |
+
border: 1px solid #16a085 !important;
|
| 201 |
+
}
|
| 202 |
+
"""
|
| 203 |
+
with gr.Blocks(theme=custom_theme, css=custom_css, title=f"KeyLock Steganography v{__version__}") as keylock_app_interface:
|
| 204 |
+
gr.Markdown(f"<div align='center' style='margin-bottom:15px;'><span style='font-size:2.5em;font-weight:bold;'>🔑 KeyLock v{__version__}</span><h2 style='font-size:1.2em;color:#bdc3c7;margin-top:5px;'>Portable API Key Wallet in a PNG</h2></div>")
|
| 205 |
+
gr.HTML("<div align='center' style='margin-bottom:10px;font-size:0.9em;color:#bdc3c7;'>Securely embed and extract API key-value pairs (or any text) within PNG images using LSB steganography and AES-256-GCM encryption.</div>")
|
| 206 |
+
gr.HTML("<div align='center' style='margin-bottom:15px;font-size:0.9em;'><span style='font-weight:bold;'>GitHub: <a href='https://github.com/broadfield-dev/KeyLock-API-Wallet' target='_blank' style='color:#1abc9c;'>KeyLock-API-Wallet</a> | Decoder Module: <a href='https://github.com/broadfield-dev/keylock-decode' target='_blank' style='color:#1abc9c;'>keylock-decode</a></span></div>")
|
| 207 |
+
gr.HTML("<hr style='border-color: rgba(189, 195, 199, 0.2); margin-bottom:25px;'>")
|
| 208 |
+
|
| 209 |
+
with gr.Tabs():
|
| 210 |
+
with gr.TabItem(f"{ICON_EMBED} Embed Data"):
|
| 211 |
+
with gr.Row():
|
| 212 |
+
with gr.Column(scale=2):
|
| 213 |
+
embed_kv_input = gr.Textbox(
|
| 214 |
+
label="Secret Data (Key:Value Pairs, one per line)",
|
| 215 |
+
placeholder="API_KEY_1: your_secret_value_1\nSERVICE_USER = '[email protected]'\n# Lines starting with # are ignored",
|
| 216 |
+
lines=7,
|
| 217 |
+
info="Enter secrets as Key:Value or Key=Value. Each pair on a new line."
|
| 218 |
+
)
|
| 219 |
+
embed_password_input = gr.Textbox(
|
| 220 |
+
label="Encryption Password",
|
| 221 |
+
type="password",
|
| 222 |
+
placeholder="Enter a strong password",
|
| 223 |
+
info="Required to encrypt data. Keep this safe!"
|
| 224 |
+
)
|
| 225 |
+
embed_output_filename_base = gr.Textbox(
|
| 226 |
+
label="Base Name for Downloaded Stego Image",
|
| 227 |
+
value="keylock_wallet",
|
| 228 |
+
info="'.png' will be appended. e.g., 'my_project_secrets'"
|
| 229 |
+
)
|
| 230 |
+
with gr.Accordion("Carrier Image Options", open=False):
|
| 231 |
+
embed_generate_carrier_checkbox = gr.Checkbox(
|
| 232 |
+
label="Generate new KeyLock Wallet image",
|
| 233 |
+
value=True,
|
| 234 |
+
info="Uncheck to upload your own PNG carrier image."
|
| 235 |
+
)
|
| 236 |
+
embed_input_image_upload = gr.Image(
|
| 237 |
+
label="Upload Your Own PNG Carrier (Optional)",
|
| 238 |
+
type="pil",
|
| 239 |
+
image_mode="RGB",
|
| 240 |
+
sources=["upload"],
|
| 241 |
+
visible=False, # Initially hidden
|
| 242 |
+
show_download_button=False,
|
| 243 |
+
interactive=True
|
| 244 |
+
)
|
| 245 |
+
embed_show_keys_checkbox = gr.Checkbox(
|
| 246 |
+
label="Show list of key names on image overlay",
|
| 247 |
+
value=True,
|
| 248 |
+
info="Displays embedded key names (not values) on the image."
|
| 249 |
+
)
|
| 250 |
+
embed_button = gr.Button("Embed Secrets "+ICON_EMBED, variant="primary")
|
| 251 |
+
|
| 252 |
+
with gr.Column(scale=3):
|
| 253 |
+
gr.Markdown("### Output Image & Status")
|
| 254 |
+
embed_output_status = gr.Textbox(
|
| 255 |
+
label="Embedding Status",
|
| 256 |
+
lines=4,
|
| 257 |
+
interactive=False,
|
| 258 |
+
placeholder="Status messages will appear here..."
|
| 259 |
+
)
|
| 260 |
+
embed_output_image_html = gr.HTML(
|
| 261 |
+
label="Preview of Stego Image (Max 480px width)",
|
| 262 |
+
value="<div style='text-align:center; color:#bdc3c7; padding:20px;'>Image preview will appear here after embedding.</div>"
|
| 263 |
+
)
|
| 264 |
+
embed_download_file = gr.File(
|
| 265 |
+
label="Download Your KeyLock Image (PNG)",
|
| 266 |
+
interactive=False,
|
| 267 |
+
file_count="single"
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
def toggle_carrier_upload(generate_flag):
|
| 271 |
+
return gr.update(visible=not generate_flag)
|
| 272 |
+
|
| 273 |
+
embed_generate_carrier_checkbox.change(
|
| 274 |
+
fn=toggle_carrier_upload,
|
| 275 |
+
inputs=[embed_generate_carrier_checkbox],
|
| 276 |
+
outputs=[embed_input_image_upload]
|
| 277 |
+
)
|
| 278 |
+
embed_button.click(
|
| 279 |
+
fn=gradio_embed_data,
|
| 280 |
+
inputs=[
|
| 281 |
+
embed_kv_input,
|
| 282 |
+
embed_password_input,
|
| 283 |
+
embed_input_image_upload,
|
| 284 |
+
embed_generate_carrier_checkbox,
|
| 285 |
+
embed_show_keys_checkbox,
|
| 286 |
+
embed_output_filename_base
|
| 287 |
+
],
|
| 288 |
+
outputs=[
|
| 289 |
+
embed_output_image_html,
|
| 290 |
+
embed_output_status,
|
| 291 |
+
embed_download_file
|
| 292 |
+
]
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
with gr.TabItem(f"{ICON_EXTRACT} Extract Data"):
|
| 296 |
+
with gr.Row():
|
| 297 |
+
with gr.Column(scale=1):
|
| 298 |
+
extract_stego_image_upload = gr.Image(
|
| 299 |
+
label="Upload KeyLock PNG Image",
|
| 300 |
+
type="pil",
|
| 301 |
+
image_mode="RGB",
|
| 302 |
+
sources=["upload"],
|
| 303 |
+
show_download_button=False,
|
| 304 |
+
interactive=True,
|
| 305 |
+
info="Upload the PNG image containing KeyLock data."
|
| 306 |
+
)
|
| 307 |
+
extract_password_input = gr.Textbox(
|
| 308 |
+
label="Decryption Password",
|
| 309 |
+
type="password",
|
| 310 |
+
placeholder="Enter the password used during embedding",
|
| 311 |
+
info="Required to decrypt and extract data."
|
| 312 |
+
)
|
| 313 |
+
extract_button = gr.Button("Extract Secrets "+ICON_EXTRACT, variant="primary")
|
| 314 |
+
|
| 315 |
+
with gr.Column(scale=2):
|
| 316 |
+
gr.Markdown("### Extracted Data & Status")
|
| 317 |
+
extract_output_status = gr.Textbox(
|
| 318 |
+
label="Extraction Status",
|
| 319 |
+
lines=2,
|
| 320 |
+
interactive=False,
|
| 321 |
+
placeholder="Status messages will appear here..."
|
| 322 |
+
)
|
| 323 |
+
extract_output_data = gr.Textbox(
|
| 324 |
+
label="Extracted Secret Data",
|
| 325 |
+
lines=10,
|
| 326 |
+
interactive=False,
|
| 327 |
+
placeholder="Extracted data (usually JSON) will appear here...",
|
| 328 |
+
show_copy_button=True
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
extract_button.click(
|
| 332 |
+
fn=gradio_extract_data,
|
| 333 |
+
inputs=[
|
| 334 |
+
extract_stego_image_upload,
|
| 335 |
+
extract_password_input
|
| 336 |
+
],
|
| 337 |
+
outputs=[
|
| 338 |
+
extract_output_data,
|
| 339 |
+
extract_output_status
|
| 340 |
+
]
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
gr.Markdown("<hr style='border-color: rgba(189, 195, 199, 0.1); margin-top: 30px; margin-bottom:10px;'>")
|
| 344 |
+
gr.Markdown(f"<div style='text-align:center; font-size:0.8em; color:#7f8c8d;'>KeyLock-API-Wallet v{__version__}. Use responsibly.</div>")
|
| 345 |
+
|
| 346 |
+
return keylock_app_interface
|
| 347 |
+
|
| 348 |
+
def main():
|
| 349 |
+
app_logger.info(f"Starting KeyLock Gradio Application v{__version__}...")
|
| 350 |
+
try:
|
| 351 |
+
ImageFont.truetype("DejaVuSans.ttf", 10)
|
| 352 |
+
app_logger.info("DejaVuSans font found, PIL font rendering should be good.")
|
| 353 |
+
except IOError:
|
| 354 |
+
try:
|
| 355 |
+
ImageFont.truetype("arial.ttf", 10)
|
| 356 |
+
app_logger.info("Arial font found, PIL font rendering should be good.")
|
| 357 |
+
except IOError:
|
| 358 |
+
app_logger.warning("Common system fonts (DejaVuSans/Arial) not found. PIL might use basic bitmap font if other preferred fonts in core.py are also unavailable. Overlay text quality might be affected.")
|
| 359 |
+
|
| 360 |
+
keylock_app_interface = build_interface()
|
| 361 |
+
|
| 362 |
+
launch_args = {"allowed_paths": [tempfile.gettempdir()]}
|
| 363 |
+
|
| 364 |
+
server_name = os.environ.get('GRADIO_SERVER_NAME')
|
| 365 |
+
server_port = os.environ.get('GRADIO_SERVER_PORT')
|
| 366 |
+
|
| 367 |
+
if server_name:
|
| 368 |
+
launch_args["server_name"] = server_name
|
| 369 |
+
app_logger.info(f"Using server_name from environment: {server_name}")
|
| 370 |
+
if server_port:
|
| 371 |
+
try:
|
| 372 |
+
launch_args["server_port"] = int(server_port)
|
| 373 |
+
app_logger.info(f"Using server_port from environment: {server_port}")
|
| 374 |
+
except ValueError:
|
| 375 |
+
app_logger.warning(f"Invalid GRADIO_SERVER_PORT: {server_port}. Using default.")
|
| 376 |
+
|
| 377 |
+
keylock_app_interface.launch(**launch_args)
|
| 378 |
|
| 379 |
if __name__ == "__main__":
|
| 380 |
+
main()
|