File size: 15,458 Bytes
9d9b9be
 
9a7a411
9d9b9be
 
 
 
 
 
 
4ece2db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02831c4
 
4ece2db
 
02831c4
 
 
 
 
 
 
 
 
 
 
 
 
4ece2db
 
 
02831c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c67331b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b89842
c67331b
1b89842
c67331b
 
1b89842
c67331b
1b89842
c67331b
1b89842
c67331b
 
 
1b89842
 
c67331b
 
 
 
1b89842
 
c67331b
 
1b89842
c67331b
 
 
1b89842
c67331b
1b89842
 
c67331b
 
1b89842
c67331b
 
1b89842
c67331b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b89842
9d9b9be
e4d97fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d9b9be
1b89842
 
 
 
9a7a411
9d9b9be
1b89842
9d9b9be
 
 
1b89842
9d9b9be
 
 
 
 
 
1b89842
9d9b9be
1b89842
 
9d9b9be
 
 
 
 
 
 
 
4412c31
 
 
 
 
 
 
 
 
 
 
aaf6dc8
 
 
4412c31
 
 
aaf6dc8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4412c31
 
 
 
 
 
 
9d9b9be
 
4ece2db
9a7a411
 
9d9b9be
1b89842
4ece2db
 
 
9a7a411
 
9d9b9be
02831c4
 
9a7a411
c67331b
 
 
9a7a411
 
 
1b89842
9a7a411
 
1b89842
9a7a411
 
1b89842
9a7a411
1
2
3
4
5
6
7
8
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
import os
import sys
import shutil
from pathlib import Path
from huggingface_hub import snapshot_download
import importlib.util

from dotenv import load_dotenv
load_dotenv()

def ensure_root_directories(main_workspace: Path) -> None:
    """Create required root-level folders and subfolders before any symlinks.

    Folders: Generated/, static/{css,js,images}/, view_session/
    Also drops a .gitkeep in each to ensure they can be tracked in a repo.
    """
    print("πŸ—οΈ  Creating root-level directories (before symlinking)...")
    required = [
        main_workspace / "Generated",
        main_workspace / "static",
        main_workspace / "static" / "css",
        main_workspace / "static" / "js",
        main_workspace / "static" / "images",
        main_workspace / "view_session",
    ]

    for d in required:
        try:
            # If a symlink exists at the path, replace it with a real directory
            if d.is_symlink():
                try:
                    d.unlink()
                    print(f"πŸ” Replaced symlink with real directory: {d}")
                except Exception as e:
                    print(f"⚠️  Could not remove symlink {d}: {e}")
            # Ensure directory exists
            d.mkdir(parents=True, exist_ok=True)
            try:
                d.chmod(0o755)
            except Exception:
                pass
            # Add README.md so directories are tracked per current .gitignore rules
            try:
                readme = d / "README.md"
                if not readme.exists():
                    rel = d.relative_to(main_workspace)
                    readme.write_text(f"# {rel}\n\nPlaceholder to ensure this directory is tracked.\n")
            except Exception:
                pass
            print(f"βœ… Ensured directory: {d}")
        except Exception as e:
            print(f"⚠️  Warning: could not ensure {d}: {e}")

def create_symlinks_to_real_folders(cache_dir):
    """Create symbolic links in cache directory to real folders in main workspace."""
    # Use the directory containing this script as root workspace
    main_workspace = Path(__file__).parent.resolve()
    
    # Define the folders that need symbolic links
    folders_to_link = ["Generated", "static", "view_session"]
    
    print("πŸ”— Creating symbolic links to real folders...")
    
    for folder_name in folders_to_link:
        # Real folder path in main workspace
        real_folder = main_workspace / folder_name
        # Symbolic link path in cache
        link_path = cache_dir / folder_name
        
        try:
            # Ensure real folder exists (defensive)
            real_folder.mkdir(parents=True, exist_ok=True)

            # Remove existing file/folder/link if it exists
            if link_path.exists() or link_path.is_symlink():
                if link_path.is_symlink():
                    link_path.unlink()
                elif link_path.is_dir():
                    shutil.rmtree(link_path)
                else:
                    link_path.unlink()
            
            # Create symbolic link
            link_path.symlink_to(real_folder, target_is_directory=True)
            print(f"βœ… Created symlink: {link_path} -> {real_folder}")
            
        except Exception as e:
            print(f"⚠️  Warning: Could not create symlink for {folder_name}: {e}")
            # Fallback: create empty directory
            link_path.mkdir(exist_ok=True)
            print(f"πŸ“ Created fallback directory: {link_path}")
    
    print("🎯 All symbolic links are ready!")
    return True

def modify_byteplus_for_directories(cache_dir):
    """Modify the BytePlus app to ensure proper directory handling."""
    app_file = cache_dir / "app.py"
    
    if not app_file.exists():
        print("⚠️  BytePlus app.py not found in cache")
        return False
    
    try:
        # Read the current app content
        with open(app_file, 'r', encoding='utf-8') as f:
            app_content = f.read()
        
        # Add directory creation code at the beginning of the app
        directory_setup_code = '''
import os
from pathlib import Path

def ensure_directories_exist():
    """Ensure required directories exist for BytePlus operation."""
    required_dirs = ["Generated", "static", "view_session"]
    
    for dir_name in required_dirs:
        dir_path = Path(dir_name)
        if not dir_path.exists():
            dir_path.mkdir(parents=True, exist_ok=True)
            print(f"Created directory: {dir_path}")
        
        # Set proper permissions
        try:
            dir_path.chmod(0o755)
        except:
            pass  # Ignore permission errors
        
        # Create subdirectories
        if dir_name == "static":
            for subdir in ["css", "js", "images"]:
                (dir_path / subdir).mkdir(exist_ok=True)
    
    print("βœ… All required directories are ready")

# Initialize directories when the app starts
ensure_directories_exist()

'''
        
        # Insert directory setup after the imports but before the main code
        import_end = app_content.find('\n# Configure secure logging')
        if import_end == -1:
            import_end = app_content.find('\n# Security Configuration')
        if import_end == -1:
            import_end = app_content.find('\nlogger = logging.getLogger')
        
        if import_end != -1:
            modified_content = (app_content[:import_end] + 
                              '\n' + directory_setup_code + 
                              app_content[import_end:])
            
            # Write the modified content back
            with open(app_file, 'w', encoding='utf-8') as f:
                f.write(modified_content)
            
            print("βœ… Modified BytePlus app to ensure directory creation")
            return True
        else:
            print("⚠️  Could not find insertion point in BytePlus app")
            return False
            
    except Exception as e:
        print(f"❌ Error modifying BytePlus app: {e}")
        return False

def create_native_symlinks_in_cache(cache_dir):
    """Create native directories directly in the cache for BytePlus to use.
    
    Since HuggingFace repo doesn't have Generated, static, view_session folders,
    we create them directly in cache with proper permissions so BytePlus can work.
    """
    directories_to_create = ["Generated", "static", "view_session"]
    
    print("Creating required directories directly in cache for BytePlus...")
    
    for dir_name in directories_to_create:
        # Directory directly in cache
        cache_dir_path = cache_dir / dir_name
        
        try:
            # Create the directory in cache if it doesn't exist
            if not cache_dir_path.exists():
                cache_dir_path.mkdir(parents=True, exist_ok=True)
                print(f"βœ… Created cache directory: {cache_dir_path}")
            
            # Set proper permissions (755 - readable/writable by owner, readable by others)
            cache_dir_path.chmod(0o755)
            print(f"βœ… Set permissions 755 on: {cache_dir_path}")
            
            # Verify the directory is accessible and writable
            if cache_dir_path.is_dir() and os.access(cache_dir_path, os.W_OK):
                print(f"βœ… Verified: {dir_name} is accessible and writable in cache")
            else:
                print(f"⚠️  Warning: {dir_name} may not be fully accessible")
                
        except Exception as e:
            print(f"❌ Error creating cache directory for {dir_name}: {e}")
            # Still try to create basic directory
            try:
                cache_dir_path.mkdir(parents=True, exist_ok=True)
                print(f"πŸ“ Created basic directory: {cache_dir_path}")
            except Exception as fe:
                print(f"❌ Complete failure for {dir_name}: {fe}")
    
    # Create additional subdirectories that might be needed
    try:
        # Create common subdirectories for static files
        (cache_dir / "static" / "css").mkdir(parents=True, exist_ok=True)
        (cache_dir / "static" / "js").mkdir(parents=True, exist_ok=True)
        (cache_dir / "static" / "images").mkdir(parents=True, exist_ok=True)
        
        # Ensure view_session has proper structure
        (cache_dir / "view_session").chmod(0o755)
        
        print("βœ… Created additional subdirectory structure")
    except Exception as e:
        print(f"⚠️  Warning: Could not create additional subdirectories: {e}")
    
    # Create a simple index file for static directory
    try:
        index_file = cache_dir / "static" / "index.html"
        if not index_file.exists():
            index_file.write_text("""<!DOCTYPE html>
<html>
<head><title>BytePlus Static Files</title></head>
<body><h1>BytePlus Image Generation Studio</h1><p>Static files directory</p></body>
</html>""")
        print("βœ… Created static index file")
    except Exception as e:
        print(f"⚠️  Warning: Could not create static index: {e}")
    
    print("🎯 Cache directory structure ready for BytePlus operation")
    return True

def setup_cache_directory():
    """Create a hidden cache directory '.cache'.

    If an existing 'cache' directory exists, migrate its contents into '.cache'.
    Set restrictive permissions (owner rwx) and on macOS set the Finder hidden flag
    unless the environment variable `HIDE_CACHE` is explicitly set to '0'.
    """
    hidden_cache = Path(".cache")
    public_cache = Path("cache")

    # If the hidden cache doesn't exist but a public one does, move it.
    try:
        if not hidden_cache.exists():
            if public_cache.exists():
                # Prefer move to preserve contents and metadata
                public_cache.rename(hidden_cache)
            else:
                hidden_cache.mkdir(exist_ok=True)
        else:
            # ensure it exists
            hidden_cache.mkdir(exist_ok=True)

        # Restrict permissions to owner only (rwx------)
        try:
            hidden_cache.chmod(0o700)
        except Exception:
            # chmod may fail on some filesystems or platforms; ignore
            pass

        # On macOS, optionally set Finder hidden flag for extra concealment
        if sys.platform == "darwin":
            hide_flag = os.environ.get("HIDE_CACHE", "1")
            if hide_flag != "0":
                try:
                    # 'chflags hidden <path>' will make the folder hidden in Finder
                    os.system(f"/usr/bin/chflags hidden {hidden_cache}")
                except Exception:
                    pass

        return hidden_cache
    except Exception:
        # Fallback: try to create a simple cache folder named 'cache'
        public_cache.mkdir(exist_ok=True)
        return public_cache

def download_space(cache_dir):
    """Download the BytePlus space from HuggingFace."""
    token = os.environ.get("HF_TOKEN")
    repo_id = os.environ.get("REPO_ID")
    
    if not token or not repo_id:
        print("❌ HF_TOKEN or REPO_ID not found in environment variables")
        return False
    
    try:
        print(f"πŸ“₯ Downloading BytePlus space: {repo_id}")
        snapshot_download(
            repo_id=repo_id,
            repo_type="space",
            local_dir=cache_dir,
            token=token
        )
        print("βœ… Successfully downloaded BytePlus space")
        return True
    except Exception as e:
        print(f"❌ Error downloading space: {e}")
        return False

def load_app(cache_dir):
    sys.path.insert(0, str(cache_dir))
    app_path = cache_dir / "app.py"
    spec = importlib.util.spec_from_file_location("app", app_path)
    app = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(app)

    # First try the common 'demo' attribute used by many Gradio Spaces
    if hasattr(app, "demo"):
        return app.demo

    # Otherwise, search for any attribute with a callable 'launch' method
    for name in dir(app):
        try:
            attr = getattr(app, name)
        except Exception:
            continue
        # Skip classes (types) since their 'launch' will be an unbound function
        if isinstance(attr, type):
            continue
        if hasattr(attr, "launch") and callable(getattr(attr, "launch")):
            return attr

    # Next, accept top-level callables that return an object with a 'launch' method.
    # This covers Spaces that expose a factory function instead of a 'demo' object.
    factory_names = [
        "create_secure_interface",
        "create_app",
        "create_demo",
        "main",
        "generator",
    ]

    for name in factory_names:
        if hasattr(app, name):
            try:
                candidate = getattr(app, name)
                if callable(candidate):
                    created = candidate()
                    if hasattr(created, "launch") and callable(getattr(created, "launch")):
                        return created
                    # If the factory itself is a Gradio Interface (callable with launch), return it
                    if hasattr(candidate, "launch") and callable(getattr(candidate, "launch")):
                        return candidate
            except Exception:
                # ignore failures from calling the factory and continue searching
                pass

    # Also accept any top-level callable that when called returns an object with 'launch'
    for name in dir(app):
        if name.startswith("__"):
            continue
        try:
            attr = getattr(app, name)
        except Exception:
            continue
        if callable(attr):
            try:
                created = attr()
                if hasattr(created, "launch") and callable(getattr(created, "launch")):
                    return created
            except Exception:
                # if calling fails, skip
                continue

    # If nothing found, raise a helpful error describing the problem
    available = [n for n in dir(app) if not n.startswith("__")]
    raise AttributeError(
        "Could not find a demo application in the downloaded space."
        f" Looked for attribute 'demo' and any attribute with a callable 'launch'."
        f" Available top-level names: {available}"
    )

if __name__ == "__main__":
    print("πŸš€ Setting up BytePlus Image Generation Studio with root directories + symlinks...")
    
    # Setup cache directory
    cache_dir = setup_cache_directory()
    print(f"πŸ“ Cache directory: {cache_dir}")

    # Ensure root directories exist first (as requested)
    ensure_root_directories(Path(__file__).parent.resolve())
    
    # Download the space
    if download_space(cache_dir):
        # Create symbolic links in cache to real folders in main workspace
        create_symlinks_to_real_folders(cache_dir)
        
        # Modify BytePlus app to ensure directory creation
        modify_byteplus_for_directories(cache_dir)
        
        # Load and launch the app
        try:
            demo = load_app(cache_dir)
            print("πŸŽ‰ Launching BytePlus Image Generation Studio...")
            demo.launch()
        except Exception as e:
            print(f"❌ Error launching app: {e}")
            sys.exit(1)
    else:
        print("❌ Failed to download space. Please check your HF_TOKEN and REPO_ID environment variables.")
        sys.exit(1)