npc0 commited on
Commit
dca593a
·
verified ·
1 Parent(s): c447b37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -257
app.py CHANGED
@@ -9,278 +9,221 @@ import openai
9
  import gradio as gr
10
  from epub2txt import epub2txt
11
 
12
- class AppLogic:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  """
14
- This class now ONLY contains the application logic (handling keys, processing files).
15
- The UI definition has been moved into the `build_ui` function.
16
  """
17
- def __init__(self, request: gr.Request):
18
- # Configuration
19
- self.model_name = os.getenv("POE_MODEL", "GPT-5-mini")
20
- self.prompt = os.getenv("prompt", "Summarize the following text:")
21
- self.client = None
22
- self.api_key = None
23
- self.keys_file = "user_keys.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- # User-specific properties derived from the request
26
- self.username = request.username
27
- self.user_id = self._get_user_id(self.username)
28
-
29
- # Initialize by loading the saved key, if it exists
30
- self.load_and_initialize_saved_key()
31
-
32
- def _get_user_id(self, username):
33
- """Generate a unique user ID from the username."""
34
- if not username:
35
- return None
36
- return hashlib.sha256(username.encode()).hexdigest()
37
 
38
- def _generate_key_from_user(self, user_id):
39
- """Generate encryption key from user ID."""
40
- salt = user_id.encode()[:32].ljust(32, b'0')
41
- kdf = PBKDF2HMAC(
42
- algorithm=hashes.SHA256(),
43
- length=32,
44
- salt=salt,
45
- iterations=100000,
46
- )
47
- key = base64.urlsafe_b64encode(kdf.derive(user_id.encode()))
48
- return Fernet(key)
49
-
50
- def _load_user_keys(self):
51
- """Load encrypted user keys from file."""
52
- if os.path.exists(self.keys_file):
53
- with open(self.keys_file, 'r') as f:
54
- return json.load(f)
55
- return {}
56
-
57
- def _save_user_keys(self, keys_data):
58
- """Save encrypted user keys to file."""
59
- with open(self.keys_file, 'w') as f:
60
- json.dump(keys_data, f)
61
-
62
- def _get_saved_key(self):
63
- """Retrieve and decrypt user's API key."""
64
- keys_data = self._load_user_keys()
65
- if self.user_id in keys_data:
66
- try:
67
- cipher_suite = self._generate_key_from_user(self.user_id)
68
- encrypted_key = base64.urlsafe_b64decode(keys_data[self.user_id].encode())
69
- return cipher_suite.decrypt(encrypted_key).decode()
70
- except Exception as e:
71
- print(f"Error retrieving saved key for {self.username}: {e}")
72
- return None
73
-
74
- def _save_encrypted_key(self, api_key):
75
- """Encrypt and save user's API key."""
76
- keys_data = self._load_user_keys()
77
- cipher_suite = self._generate_key_from_user(self.user_id)
78
- encrypted_key = cipher_suite.encrypt(api_key.encode())
79
- keys_data[self.user_id] = base64.urlsafe_b64encode(encrypted_key).decode()
80
- self._save_user_keys(keys_data)
81
- return True
82
-
83
- def _delete_saved_key(self):
84
- """Delete user's saved API key."""
85
- keys_data = self._load_user_keys()
86
- if self.user_id in keys_data:
87
- del keys_data[self.user_id]
88
- self._save_user_keys(keys_data)
89
- return True
90
-
91
- def _initialize_client(self):
92
- """Initialize the Poe API client if an API key is present."""
93
- if not self.api_key:
94
- return False
95
- try:
96
- self.client = openai.OpenAI(
97
- api_key=self.api_key,
98
- base_url="https://api.poe.com/v1",
99
- )
100
- # A quick check to see if the client is functional without making a call
101
- return hasattr(self.client, 'chat')
102
- except Exception as e:
103
- print(f"Error initializing Poe client for {self.username}: {e}")
104
- return False
105
 
106
- def load_and_initialize_saved_key(self):
107
- """Checks for a saved key and initializes the client if found."""
108
- saved_key = self._get_saved_key()
109
- if saved_key:
110
- self.api_key = saved_key
111
- self._initialize_client()
112
 
113
- def set_api_key(self, api_key, remember_key):
114
- """Set and validate the API key."""
115
- if not api_key or not api_key.strip():
116
- return (
117
- gr.update(value="⚠️ Please enter a valid API key."),
118
- gr.update(visible=False),
119
- gr.update(visible=True),
120
- gr.update(visible=False),
121
- )
122
-
123
- self.api_key = api_key.strip()
124
-
125
- if self._initialize_client():
126
- try:
127
- # Test the API key with a simple request
128
- self.client.chat.completions.create(
129
- model=self.model_name, messages=[{"role": "user", "content": "Hello"}], max_tokens=10
130
- )
131
-
132
- if remember_key:
133
- self._save_encrypted_key(self.api_key)
134
- msg = "✅ API key validated and saved! You can now upload an ePub file."
135
- else:
136
- self._delete_saved_key() # Ensure no old key is lingering if user unchecks
137
- msg = "✅ API key validated for this session! You can now upload an ePub file."
138
 
139
- return (
140
- gr.update(value=msg),
141
- gr.update(visible=True), # Show file input
142
- gr.update(visible=False), # Hide API key section
143
- gr.update(visible=remember_key), # Show clear button only if saved
144
- )
145
- except Exception as e:
146
- self.client = None # Reset client on failure
147
- error_msg = str(e)
148
- if "401" in error_msg:
149
- return gr.update(value="❌ Invalid API key. Please check it and try again."), gr.update(), gr.update(), gr.update()
150
- return gr.update(value=f"❌ API connection error: {error_msg}"), gr.update(), gr.update(), gr.update()
151
- else:
152
- return gr.update(value="❌ Failed to initialize API client."), gr.update(), gr.update(), gr.update()
153
 
154
- def clear_saved_key(self):
155
- """Clear the user's saved API key."""
156
- self._delete_saved_key()
157
- self.api_key = None
158
- self.client = None
159
- return (
160
- gr.update(value="✅ Saved API key cleared. Please enter your API key to continue."),
161
- gr.update(visible=True), # Show API key section
162
- gr.update(visible=False), # Hide clear button
163
- gr.update(value=""), # Clear API key input field
164
- gr.update(visible=False), # Hide file input
165
- )
166
-
167
- # ... (get_model_response and process methods are unchanged) ...
168
- def get_model_response(self, text: str) -> str:
169
- """Get response from Poe API for the given text."""
170
- if not self.client:
171
- return "Error: API client not initialized. Please set your API key."
172
- try:
173
- chat = self.client.chat.completions.create(
174
- model=self.model_name,
175
- messages=[{"role": "user", "content": text}],
176
- )
177
- return chat.choices[0].message.content
178
- except Exception as e:
179
- return f"Error calling Poe API: {str(e)}"
180
 
181
- def process(self, file):
182
- """Processes the uploaded ePub file."""
183
- if not self.client:
184
- yield gr.update(value='⚠️ Please set your Poe API key first.')
185
- return
186
- if file is None:
187
- yield gr.update(value='Please upload an ePub file.')
188
- return
189
 
190
- try:
191
- ch_list = epub2txt(file.name, outputlist=True)
192
- chapter_titles = epub2txt.content_titles
193
- title = epub2txt.title
194
-
195
- yield gr.update(value=f"# {title}\n\nProcessing ePub file...")
196
- sm_list = []
197
-
198
- # Skip first 2 chapters (usually metadata)
199
- for idx, text in enumerate(ch_list[2:], 1):
200
- if not text.strip(): continue
201
- yield gr.update(value=f"# {title}\n\nProcessing chapter {idx}...")
202
-
203
- docs = []
204
- chunk_size = 2000
205
- for i in range(0, len(text), chunk_size):
206
- chunk = text[i:i + 2048]
207
- if len(chunk.strip()) > 0:
208
- response = self.get_model_response(self.prompt + "\n\n" + chunk)
209
- docs.append(response)
210
-
211
- if docs:
212
- hist = docs[0]
213
- for doc in docs[1:]:
214
- combined_text = f"{self.prompt}\n\nCombine these summaries:\n\n{hist}\n\n{doc}"
215
- hist = self.get_model_response(combined_text)
216
- sm_list.append(hist)
217
-
218
- final_summaries = "\n\n".join([f"## {ct}\n\n{sm}" for ct, sm in zip(chapter_titles[2:idx+1], sm_list)])
219
- yield gr.update(value=f"# {title}\n\n{final_summaries}")
220
-
221
- if sm_list:
222
- complete_summary = f"# {title}\n\n" + "\n\n".join([f"## {ct}\n\n{sm}" for ct, sm in zip(chapter_titles[2:len(sm_list)+2], sm_list)])
223
- yield gr.update(value=complete_summary)
224
- else:
225
- yield gr.update(value=f"# {title}\n\nNo content found to summarize.")
226
-
227
- except Exception as e:
228
- yield gr.update(value=f"Error processing file: {str(e)}")
229
 
230
- # --- UI Building Function ---
231
- def build_ui(request: gr.Request):
232
- """
233
- This function is the main entry point for the UI.
234
- It's only called AFTER the user has logged in.
235
- """
236
- # Instantiate the logic class for this specific user session
237
- logic = AppLogic(request)
238
-
239
- # Determine the initial state based on whether a key was loaded
240
- has_saved_key = logic.client is not None
241
-
242
- welcome_message = (f"# ePub Summarization Tool\n\nWelcome back, **{logic.username}**! "
243
- "✅ Your saved API key is loaded and ready.") if has_saved_key else \
244
- (f"# ePub Summarization Tool\n\nWelcome, **{logic.username}**! "
245
- "Please set up your Poe API key to begin.")
246
-
247
- with gr.Blocks(title="ePub Summarizer") as demo:
248
- gr.Markdown(welcome_message)
249
-
250
- # API Key input section
251
- with gr.Column(visible=not has_saved_key) as api_key_section:
252
- api_key_input = gr.Textbox(label="Poe API Key", type="password")
253
- remember_key = gr.Checkbox(label="Remember my API key", value=True)
254
- api_key_btn = gr.Button("Set API Key", variant="primary")
255
-
256
- # Main content area
257
- out = gr.Markdown()
258
- inp = gr.File(file_types=['.epub'], visible=has_saved_key, label="Upload ePub File")
259
- clear_key_btn = gr.Button("Clear Saved Key", variant="secondary", visible=has_saved_key)
260
 
261
- # --- Event Handlers ---
262
- api_key_btn.click(
263
- logic.set_api_key,
264
- inputs=[api_key_input, remember_key],
265
- outputs=[out, inp, api_key_section, clear_key_btn]
266
- )
267
- clear_key_btn.click(
268
- logic.clear_saved_key,
269
- outputs=[out, api_key_section, clear_key_btn, api_key_input, inp]
270
- )
271
- inp.change(logic.process, inputs=[inp], outputs=[out])
272
 
273
- return demo
274
 
275
- # --- Main Application Execution ---
276
  if __name__ == "__main__":
277
- # This dummy function is for local testing. On Hugging Face, it's ignored.
 
278
  def auth(username, password):
279
  return True
280
 
281
- # Use gr.mount_gradio_app for FastAPI integration if needed, or just launch directly
282
- with gr.Blocks() as demo:
283
- # The auth() method gates the UI. build_ui is only called after successful login.
284
- demo.auth(build_ui, auth)
285
-
286
- demo.queue().launch(show_error=True)
 
9
  import gradio as gr
10
  from epub2txt import epub2txt
11
 
12
+ # --- Configuration and Constants ---
13
+ MODEL_NAME = os.getenv("POE_MODEL", "GPT-5-mini")
14
+ PROMPT = os.getenv("prompt", "Summarize the following text:")
15
+ KEYS_FILE = "user_keys.json"
16
+
17
+ # --- Helper Functions for Security and User Management ---
18
+ # By moving logic out of a class, we avoid complex state issues.
19
+
20
+ def get_user_id(username):
21
+ """Generates a unique, stable ID from a username."""
22
+ return hashlib.sha256(username.encode()).hexdigest()
23
+
24
+ def generate_fernet_key(user_id):
25
+ """Generates a deterministic encryption key from a user ID."""
26
+ salt = user_id.encode()[:16].ljust(16, b'\0') # Salt must be 16 bytes
27
+ kdf = PBKDF2HMAC(
28
+ algorithm=hashes.SHA256(),
29
+ length=32,
30
+ salt=salt,
31
+ iterations=100000,
32
+ )
33
+ key = base64.urlsafe_b64encode(kdf.derive(user_id.encode()))
34
+ return Fernet(key)
35
+
36
+ def load_user_keys():
37
+ """Loads the entire user key database."""
38
+ if os.path.exists(KEYS_FILE):
39
+ with open(KEYS_FILE, 'r') as f:
40
+ return json.load(f)
41
+ return {}
42
+
43
+ def save_user_keys(keys_data):
44
+ """Saves the entire user key database."""
45
+ with open(KEYS_FILE, 'w') as f:
46
+ json.dump(keys_data, f)
47
+
48
+ def get_decrypted_api_key(user_id):
49
+ """Retrieves and decrypts a single user's API key."""
50
+ keys_data = load_user_keys()
51
+ encrypted_key_b64 = keys_data.get(user_id)
52
+ if encrypted_key_b64:
53
+ try:
54
+ cipher = generate_fernet_key(user_id)
55
+ encrypted_key = base64.urlsafe_b64decode(encrypted_key_b64.encode())
56
+ return cipher.decrypt(encrypted_key).decode()
57
+ except Exception as e:
58
+ print(f"Decryption failed for user {user_id}: {e}")
59
+ return None
60
+
61
+ def set_encrypted_api_key(user_id, api_key):
62
+ """Encrypts and saves a single user's API key."""
63
+ keys_data = load_user_keys()
64
+ cipher = generate_fernet_key(user_id)
65
+ encrypted_key = cipher.encrypt(api_key.encode())
66
+ keys_data[user_id] = base64.urlsafe_b64encode(encrypted_key).decode()
67
+ save_user_keys(keys_data)
68
+
69
+ def delete_api_key(user_id):
70
+ """Deletes a user's API key."""
71
+ keys_data = load_user_keys()
72
+ if user_id in keys_data:
73
+ del keys_data[user_id]
74
+ save_user_keys(keys_data)
75
+
76
+ # --- Gradio Event Handlers ---
77
+
78
+ def initialize_client(api_key):
79
+ """Initializes and returns an OpenAI client or None on failure."""
80
+ if not api_key:
81
+ return None
82
+ try:
83
+ client = openai.OpenAI(api_key=api_key, base_url="https://api.poe.com/v1")
84
+ # Test connection
85
+ client.models.list()
86
+ return client
87
+ except Exception as e:
88
+ print(f"Failed to initialize client: {e}")
89
+ return None
90
+
91
+ def update_ui_on_load(request: gr.Request):
92
  """
93
+ This function runs after the user logs in and the page loads.
94
+ It configures the UI based on whether the user has a saved API key.
95
  """
96
+ user_id = get_user_id(request.username)
97
+ api_key = get_decrypted_api_key(user_id)
98
+ client = initialize_client(api_key)
99
+
100
+ if client:
101
+ # Key is valid and loaded
102
+ welcome_msg = f"Welcome back, **{request.username}**! Your saved API key is loaded."
103
+ return {
104
+ welcome_md: gr.update(value=welcome_msg),
105
+ api_key_section: gr.update(visible=False),
106
+ inp: gr.update(visible=True),
107
+ clear_key_btn: gr.update(visible=True)
108
+ }
109
+ else:
110
+ # No key or invalid key found
111
+ welcome_msg = f"Welcome, **{request.username}**! Please provide your Poe API key to begin."
112
+ return {
113
+ welcome_md: gr.update(value=welcome_msg),
114
+ api_key_section: gr.update(visible=True),
115
+ inp: gr.update(visible=False),
116
+ clear_key_btn: gr.update(visible=False)
117
+ }
118
+
119
+ def set_api_key(api_key, remember_key, request: gr.Request):
120
+ """Handles the 'Set API Key' button click."""
121
+ user_id = get_user_id(request.username)
122
+ client = initialize_client(api_key)
123
+
124
+ if not client:
125
+ return {out: gr.update(value="❌ Invalid or incorrect API key. Please check it and try again.")}
126
+
127
+ if remember_key:
128
+ set_encrypted_api_key(user_id, api_key)
129
+ msg = "✅ API key validated and saved! You can now upload an ePub."
130
+ else:
131
+ delete_api_key(user_id) # Ensure no old key is stored if user unchecks
132
+ msg = "✅ API key validated for this session. You can now upload an ePub."
133
+
134
+ return {
135
+ out: gr.update(value=msg),
136
+ inp: gr.update(visible=True),
137
+ api_key_section: gr.update(visible=False),
138
+ clear_key_btn: gr.update(visible=remember_key)
139
+ }
140
+
141
+ def clear_saved_key(request: gr.Request):
142
+ """Handles the 'Clear Saved Key' button click."""
143
+ user_id = get_user_id(request.username)
144
+ delete_api_key(user_id)
145
+ return {
146
+ out: gr.update(value="✅ Saved API key cleared. Please enter an API key to continue."),
147
+ api_key_section: gr.update(visible=True),
148
+ inp: gr.update(visible=False),
149
+ clear_key_btn: gr.update(visible=False),
150
+ api_key_input: gr.update(value="")
151
+ }
152
+
153
+ def process_epub(file, request: gr.Request):
154
+ """Processes the uploaded ePub file. This is a generator function."""
155
+ user_id = get_user_id(request.username)
156
+ api_key = get_decrypted_api_key(user_id)
157
+
158
+ # Re-initialize client to ensure API key is available for this long-running task
159
+ client = initialize_client(api_key)
160
+ if not client:
161
+ yield "⚠️ Your API key is missing or invalid. Please clear and set your key again."
162
+ return
163
+
164
+ # ... (The rest of your ePub processing logic is identical) ...
165
+ try:
166
+ ch_list = epub2txt(file.name, outputlist=True)
167
+ title = epub2txt.title
168
+ yield f"# {title}\n\nProcessing ePub file..."
169
+ sm_list = []
170
+ for text in ch_list[2:]:
171
+ if text.strip():
172
+ response = client.chat.completions.create(
173
+ model=MODEL_NAME,
174
+ messages=[{"role": "user", "content": PROMPT + "\n\n" + text[:4000]}], # Simple chunking
175
+ ).choices[0].message.content
176
+ sm_list.append(response)
177
 
178
+ yield f"# {title}\n\n" + "\n\n---\n\n".join(sm_list)
 
 
 
 
 
 
 
 
 
 
 
179
 
180
+ except Exception as e:
181
+ yield f"An error occurred: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
 
 
 
 
 
 
183
 
184
+ # --- Build the Gradio Interface ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ with gr.Blocks(title="ePub Summarizer") as demo:
187
+ welcome_md = gr.Markdown("Welcome! Please log in to continue.")
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ # API Key Section (initially hidden, made visible by update_ui_on_load)
190
+ with gr.Column(visible=False) as api_key_section:
191
+ api_key_input = gr.Textbox(label="Poe API Key", type="password")
192
+ remember_key = gr.Checkbox(label="Remember my API key", value=True)
193
+ api_key_btn = gr.Button("Set API Key", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ # Main App Section
196
+ out = gr.Markdown()
197
+ inp = gr.File(file_types=['.epub'], visible=False, label="Upload ePub File")
198
+ clear_key_btn = gr.Button("Clear Saved Key", variant="secondary", visible=False)
 
 
 
 
199
 
200
+ # --- Event Wiring ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ # 1. After login, the 'load' event fires, calling update_ui_on_load
203
+ demo.load(
204
+ fn=update_ui_on_load,
205
+ outputs=[welcome_md, api_key_section, inp, clear_key_btn]
206
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ # 2. User interactions trigger other handlers
209
+ api_key_btn.click(
210
+ fn=set_api_key,
211
+ inputs=[api_key_input, remember_key],
212
+ outputs=[out, inp, api_key_section, clear_key_btn]
213
+ )
214
+ clear_key_btn.click(
215
+ fn=clear_saved_key,
216
+ outputs=[out, api_key_section, inp, clear_key_btn, api_key_input]
217
+ )
218
+ inp.change(fn=process_epub, inputs=[inp], outputs=[out])
219
 
220
+ # --- Main Execution ---
221
 
 
222
  if __name__ == "__main__":
223
+ # This dummy function is used for local testing.
224
+ # On Hugging Face Spaces, their login system is used automatically.
225
  def auth(username, password):
226
  return True
227
 
228
+ # The 'auth' parameter is passed to launch(), not as a separate method call.
229
+ demo.queue().launch(auth=auth, show_error=True)