File size: 10,513 Bytes
284f009
7844438
284f009
7844438
 
 
 
 
 
dd47d94
 
 
 
7844438
 
284f009
 
 
 
7844438
284f009
 
0fc713e
 
 
284f009
0fc713e
284f009
 
0fc713e
 
 
 
 
 
 
 
 
 
 
 
284f009
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10d49cc
284f009
 
 
10d49cc
284f009
10d49cc
284f009
10d49cc
284f009
 
 
 
 
0fc713e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca34b49
d69f126
ca34b49
 
 
 
 
 
 
 
 
 
 
9981eb3
 
 
 
 
 
 
 
 
 
0fc713e
 
 
 
d69f126
0fc713e
 
d69f126
0fc713e
 
d69f126
0fc713e
 
 
 
 
 
 
 
 
 
 
 
 
ca34b49
 
 
 
0fc713e
 
d69f126
 
 
284f009
0fc713e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284f009
0fc713e
 
 
 
7844438
 
 
 
 
 
 
 
 
0fc713e
7844438
 
0fc713e
 
 
 
 
 
 
 
284f009
0fc713e
7844438
 
0fc713e
7844438
0fc713e
0ae4d18
 
 
0fc713e
7844438
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0fc713e
7844438
 
0fc713e
7844438
 
0fc713e
7844438
 
 
 
 
 
 
 
 
 
284f009
 
 
 
82b453d
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

# ========= Headless / Offscreen safety (before any VTK import) =========
import os
# Set headless rendering environment variables BEFORE any VTK/pyvista imports
os.environ.setdefault("VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN", "1")
os.environ.setdefault("LIBGL_ALWAYS_SOFTWARE", "1")
os.environ.setdefault("MESA_GL_VERSION_OVERRIDE", "3.3")
# Use dummy DISPLAY value instead of empty string to avoid X server connection attempts
if "DISPLAY" not in os.environ or os.environ.get("DISPLAY") == "":
    # Disable all X11 attempts
    os.environ["DISPLAY"] = ":0"
    os.environ["VTK_USE_X"] = "NO"

os.environ.setdefault("PYVISTA_OFF_SCREEN", "True")

import sys
import tempfile
from pathlib import Path
import traceback
import signal


# Writable base dir (matches alphaLPFM pattern)
# Use APP_DATA_DIR to match alphaLPFM's expectation
DATA_DIR = os.environ.get("APP_DATA_DIR", os.path.join(tempfile.gettempdir(), "appdata_trame"))
os.makedirs(DATA_DIR, exist_ok=True)
os.environ.setdefault("APP_DATA_DIR", DATA_DIR)  # Ensure it's set for the private app
os.environ.setdefault("MPLCONFIGDIR", DATA_DIR)

# Set up shared directories structure (matches alphaLPFM)
SHARED_DIR = os.path.join(DATA_DIR, "shared")
SHARED_HF_DIR = os.path.join(SHARED_DIR, "hf_home")
SHARED_WEIGHTS_DIR = os.path.join(SHARED_DIR, "weights")
for d in (SHARED_DIR, SHARED_HF_DIR, SHARED_WEIGHTS_DIR):
    os.makedirs(d, exist_ok=True)

# Set HF cache environment variables (matches alphaLPFM)
os.environ.setdefault("HF_HOME", SHARED_HF_DIR)
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", SHARED_HF_DIR)
os.environ.setdefault("TRANSFORMERS_CACHE", SHARED_HF_DIR)

def setup_environment():
    cache_dir = Path("cache")
    cache_dir.mkdir(exist_ok=True)
    if str(cache_dir.absolute()) not in sys.path:
        sys.path.insert(0, str(cache_dir.absolute()))
    return cache_dir

def download_private_repo(cache_dir, repo_id, hf_token):
    from huggingface_hub import snapshot_download
    import shutil
    import os
    print(f"πŸ” Downloading repo {repo_id}")
    repo_path = snapshot_download(
        repo_id=repo_id,
        token=hf_token,
        repo_type="space",
        local_dir=cache_dir,
        local_dir_use_symlinks=False,
    )
    
    # Copy everything from the repository to current working directory
    print(f"πŸ“ Copying all files and directories from {repo_path} to current directory")
    
    # Get all items in the repository
    for item in os.listdir(repo_path):
        src_path = os.path.join(repo_path, item)
        dst_path = item
        
        # Skip hidden files and directories (like .git, .gitignore, etc.)
        if item.startswith('.'):
            print(f"⏭️ Skipping hidden item: {item}")
            continue
            
        if os.path.isdir(src_path):
            # print(f"πŸ“ Copying directory: {item}")
            if os.path.exists(dst_path):
                shutil.rmtree(dst_path)
            shutil.copytree(src_path, dst_path)
            # print(f"βœ… Directory {item} copied successfully")
        elif os.path.isfile(src_path):
            # print(f"πŸ“„ Copying file: {item}")
            shutil.copy2(src_path, dst_path)
            # print(f"βœ… File {item} copied successfully")
    
    print(f"πŸŽ‰ All repository contents copied to current working directory")
    
    return repo_path

# Cache for loaded private app
_PRIVATE_APP_CACHE = None
_PRIVATE_APP_CREATE_APP = None

def _load_private_app_once():
    """Load the private app once and cache it. Called by create_app() or main()."""
    global _PRIVATE_APP_CACHE, _PRIVATE_APP_CREATE_APP
    
    if _PRIVATE_APP_CACHE is not None:
        return _PRIVATE_APP_CACHE, _PRIVATE_APP_CREATE_APP
    
    hf_token = os.environ.get("HF_TOKEN")
    repo_id = os.environ.get("REPO_ID")

    if not hf_token or not repo_id:
        raise ValueError("HF_TOKEN and REPO_ID must be set")

    print(f"πŸ” Loading private app from repo_id: {repo_id}")
    cache_dir = setup_environment()
    repo_path = download_private_repo(cache_dir, repo_id, hf_token)
    
    # Import from repo_path directly using importlib to avoid importing wrapper's app.py
    try:
        import importlib.util
        app_file_path = os.path.join(repo_path, "app.py")
        if not os.path.exists(app_file_path):
            raise FileNotFoundError(f"app.py not found in {repo_path}")
        
        spec = importlib.util.spec_from_file_location("private_app_module", app_file_path)
        if spec is None or spec.loader is None:
            raise ImportError(f"Could not load spec from {app_file_path}")
        
        private_app = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(private_app)
        # If the private app exposes a thumbnail generator, run it once
        # so example images are ready even when we bypass its main().
        try:
            if hasattr(private_app, "generate_example_thumbnails"):
                print("πŸ–Ό  Generating example thumbnails in private app...")
                private_app.generate_example_thumbnails()
        except Exception:
            # Thumbnails are optional; don't break startup if this fails.
            traceback.print_exc()

        # βœ… PREFER create_app factory (for multi-user session management)
        if hasattr(private_app, "create_app"):
            print("βœ… Found create_app factory in private app (supports multi-user sessions)")
            _PRIVATE_APP_CREATE_APP = private_app.create_app
        # Check for app or demo attribute
        elif hasattr(private_app, "app"):
            _PRIVATE_APP_CREATE_APP = lambda: private_app.app
        elif hasattr(private_app, "demo"):
            _PRIVATE_APP_CREATE_APP = lambda: private_app.demo
        # Check if PFMDemo class exists and instantiate it (fallback, but not ideal for multi-user)
        elif hasattr(private_app, "PFMDemo"):
            print("⚠️  Using PFMDemo directly (may not support multi-user sessions)")
            def _create_pfmdemo_app(server=None):
                from trame.app import get_server
                if server is None:
                    server = get_server()
                app_instance = private_app.PFMDemo(server)
                app_instance.server.controller.add("decimate_again", app_instance.decimate_again)
                app_instance.server.controller.add("reset_mesh", app_instance.reset_mesh)
                return app_instance
            _PRIVATE_APP_CREATE_APP = _create_pfmdemo_app
        else:
            raise RuntimeError("No demo/app/create_app found in private repo")
        
        # Also add repo_path to sys.path for any relative imports the private app might need
        if str(repo_path) not in sys.path:
            sys.path.insert(0, str(repo_path))
        
        _PRIVATE_APP_CACHE = (private_app, repo_path)
        return _PRIVATE_APP_CACHE, _PRIVATE_APP_CREATE_APP
    except Exception as e:
        traceback.print_exc()
        raise RuntimeError(f"Failed to load app from private repo: {e}")

def create_app(server=None, **kwargs):
    """
    Trame application factory (matches alphaLPFM pattern).
    
    This function is called by trame.tools.serve to create the app instance.
    It loads the private app and returns its create_app result (or wraps it).
    """
    _, create_app_fn = _load_private_app_once()
    
    if create_app_fn is None:
        raise RuntimeError("No create_app function found in private app")
    
    # Call the private app's create_app (or wrapper)
    return create_app_fn(server=server, **kwargs)

def main():
    """
    Main entry point (fallback if not using trame.tools.serve).
    This is kept for backward compatibility but trame.tools.serve is preferred.
    """
    # Setup signal handlers for graceful shutdown
    def signal_handler(sig, frame):
        print("\nπŸ›‘ Received shutdown signal, cleaning up...")
        sys.exit(0)
    
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    
    try:
        print(f"πŸš€ Starting app (using main() - consider using trame.tools.serve for multi-user support)")
        print(f"πŸ“‚ Current working directory: {os.getcwd()}")
        
        # Load private app (will be cached)
        _, create_app_fn = _load_private_app_once()
        
        if create_app_fn is None:
            raise RuntimeError("No create_app function found in private app")
        
        # Create app instance
        demo = create_app()

        # Check if it's a Trame app (has server.start method)
        if hasattr(demo, 'server') and hasattr(demo.server, 'start'):
            # Trame app
            print(f"πŸš€ Starting Trame server (host/port from trame.tools.serve or defaults)")
            
            if hasattr(demo.server.state, 'health') and demo.server.state.health == "running":
                print("Trame server already running, not starting again.")
                return

            # Configure server - host/port come from trame.tools.serve or environment
            try:
                # Try with host parameter first (if supported)
                demo.server.start(
                    open_browser=False,
                    show_connection_info=True,
                    backend="aiohttp",
                    exec_mode="main",
                )
            except TypeError:
                # If host parameter is not supported, configure via server options
                # Most Trame versions bind to 0.0.0.0 by default with aiohttp
                demo.server.start(
                    open_browser=False,
                    show_connection_info=True,
                    backend="aiohttp",
                    exec_mode="main",
                )
        elif hasattr(demo, 'launch'):
            # Gradio app
            _, repo_path = _PRIVATE_APP_CACHE if _PRIVATE_APP_CACHE else (None, None)
            demo.launch(
                server_name="0.0.0.0",
                server_port=7860,
                share=False,
                show_error=True,
                allowed_paths=[str(repo_path) if repo_path else ".", DATA_DIR]
            )
        else:
            raise RuntimeError("App does not have launch() or server.start() method")
    except KeyboardInterrupt:
        print("\nπŸ›‘ Keyboard interrupt received, shutting down...")
        sys.exit(0)
    except Exception as e:
        print(f"❌ Fatal error: {e}")
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()