Kyosuke Ichikawa commited on
Commit
fbdb7a1
·
unverified ·
1 Parent(s): 376495a

refactor: remove redundant browser_state update methods (#9)

Browse files

- Remove unused update_browser_state_audio_status method
- Remove redundant update_browser_state_extracted_text method
- Improve ensure_browser_state_completeness to handle empty app_session_id
- Consolidate browser_state management to use UserSession as single source of truth
- Update tests to reflect new behavior where UserSession defaults are used to complete browser_state
- All unit tests and E2E tests passing

This change eliminates duplicate default value definitions and improves code maintainability
by centralizing browser_state management in the UserSession class.

tests/unit/test_browser_state_management.py CHANGED
@@ -46,8 +46,17 @@ class TestBrowserStateManagement:
46
  assert user_session.session_id == existing_session_id
47
  assert updated_browser_state["app_session_id"] == existing_session_id
48
 
49
- # Browser state should remain unchanged
50
- assert updated_browser_state == browser_state_with_session
 
 
 
 
 
 
 
 
 
51
 
52
  def test_session_independence_from_gradio_hash(self):
53
  """Test that app session ID remains stable even when Gradio session hash changes."""
 
46
  assert user_session.session_id == existing_session_id
47
  assert updated_browser_state["app_session_id"] == existing_session_id
48
 
49
+ # Browser state should be completed with UserSession defaults while preserving existing values
50
+ # Check that original values are preserved
51
+ assert updated_browser_state["ui_state"]["podcast_text"] == "Previous session content"
52
+ assert updated_browser_state["ui_state"]["terms_agreed"] is True
53
+ assert updated_browser_state["user_settings"]["current_api_type"] == "openai"
54
+ assert updated_browser_state["user_settings"]["character1"] == "Kyushu Sora"
55
+
56
+ # Check that missing fields are filled with defaults
57
+ assert "current_script" in updated_browser_state["audio_generation_state"]
58
+ assert "character2" in updated_browser_state["user_settings"]
59
+ assert "document_type" in updated_browser_state["user_settings"]
60
 
61
  def test_session_independence_from_gradio_hash(self):
62
  """Test that app session ID remains stable even when Gradio session hash changes."""
tests/unit/test_ui_initialization.py CHANGED
@@ -212,62 +212,24 @@ class TestAudioStateUpdate:
212
  """Set up test fixtures."""
213
  self.app = PaperPodcastApp()
214
 
215
- def test_update_browser_state_audio_status(self):
216
- """Test updating audio status in BrowserState."""
217
- from yomitalk.user_session import UserSession
218
-
219
- user_session = UserSession()
220
  browser_state = {
221
- "app_session_id": user_session.session_id,
222
- "audio_generation_state": {
223
- "is_generating": True,
224
- "progress": 0.5,
225
- "status": "generating",
226
- "current_script": "Test script",
227
- "final_audio_path": None,
228
- "streaming_parts": ["part1.wav"],
229
- "generation_id": "gen123",
230
- },
231
  "user_settings": {},
232
- "ui_state": {},
233
  }
234
 
235
- # Update the browser state audio status
236
- updated_state = self.app.update_browser_state_audio_status(user_session, browser_state)
237
-
238
- # Verify the audio generation state is preserved/updated
239
- audio_state = updated_state["audio_generation_state"]
240
- assert "is_generating" in audio_state
241
- assert "progress" in audio_state
242
- assert "status" in audio_state
243
- assert "current_script" in audio_state
244
-
245
- def test_update_browser_state_audio_status_with_none_session(self):
246
- """Test audio status update handles None user session gracefully."""
247
- browser_state = {"app_session_id": "test-uuid", "audio_generation_state": {}, "user_settings": {}, "ui_state": {}}
248
-
249
- # Should handle None session gracefully - create dummy user session for testing
250
- user_session = Mock()
251
- user_session.session_id = "test-uuid"
252
- user_session.is_audio_generating.return_value = False
253
- user_session.get_audio_generation_progress.return_value = 0.0
254
- user_session.get_audio_generation_status.return_value = {
255
- "is_generating": False,
256
- "progress": 0.0,
257
- "status": "idle",
258
- "current_script": "",
259
- "final_audio_path": None,
260
- "streaming_parts": [],
261
- "generation_id": None,
262
- "start_time": None,
263
- "estimated_total_parts": 1,
264
- }
265
- user_session.get_current_script.return_value = ""
266
- user_session.get_final_audio_path.return_value = None
267
- user_session.get_streaming_audio_parts.return_value = []
268
 
269
- updated_state = self.app.update_browser_state_audio_status(user_session, browser_state)
 
 
 
270
 
271
- # Should return updated state with audio information
272
- assert updated_state is not browser_state # Different object
273
  assert "audio_generation_state" in updated_state
 
 
212
  """Set up test fixtures."""
213
  self.app = PaperPodcastApp()
214
 
215
+ def test_update_browser_state_ui_content(self):
216
+ """Test updating UI content in BrowserState."""
 
 
 
217
  browser_state = {
218
+ "app_session_id": "test-session",
219
+ "audio_generation_state": {},
 
 
 
 
 
 
 
 
220
  "user_settings": {},
221
+ "ui_state": {"podcast_text": "old text", "terms_agreed": False},
222
  }
223
 
224
+ # Update UI content
225
+ updated_state = self.app.update_browser_state_ui_content(browser_state, "new podcast text", True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ # Verify UI state was updated
228
+ ui_state = updated_state["ui_state"]
229
+ assert ui_state["podcast_text"] == "new podcast text"
230
+ assert ui_state["terms_agreed"] is True
231
 
232
+ # Verify other sections are preserved
233
+ assert updated_state["app_session_id"] == "test-session"
234
  assert "audio_generation_state" in updated_state
235
+ assert "user_settings" in updated_state
yomitalk/app.py CHANGED
@@ -64,37 +64,26 @@ class PaperPodcastApp:
64
  # Restore settings from browser state
65
  user_session.update_settings_from_browser_state(browser_state)
66
 
67
- # Return browser state as-is since it contains all needed state
68
- return user_session, browser_state
 
 
69
  else:
70
  # Create new session with UUID-based ID
71
  user_session = UserSession() # Will generate new UUID
72
  logger.info(f"User session initialized: {user_session.session_id}")
73
 
74
- # Initialize browser state with new session ID
75
- browser_state["app_session_id"] = user_session.session_id
76
-
77
- # Sync current settings to browser state
78
- browser_state = user_session.sync_settings_to_browser_state(browser_state)
79
-
80
- return user_session, browser_state
81
-
82
- def update_browser_state_audio_status(self, user_session: UserSession, browser_state: Dict[str, Any]) -> Dict[str, Any]:
83
- """Update BrowserState with current audio generation status."""
84
- if user_session is None:
85
- return browser_state.copy()
86
-
87
- audio_status = user_session.get_audio_generation_status(browser_state)
88
 
89
- # Create a copy of browser state and update audio generation section
90
- updated_state = browser_state.copy()
91
- updated_state["audio_generation_state"] = browser_state["audio_generation_state"].copy()
92
- updated_state["audio_generation_state"].update(audio_status)
93
-
94
- return updated_state
95
 
96
  def update_browser_state_ui_content(self, browser_state: Dict[str, Any], podcast_text: str, terms_agreed: bool, extracted_text: str = "") -> Dict[str, Any]:
97
- """Update BrowserState with UI content for recovery."""
 
 
 
 
98
  updated_state = browser_state.copy()
99
 
100
  # Update ui_state section in the new BrowserState structure
@@ -1421,34 +1410,8 @@ class PaperPodcastApp:
1421
  )
1422
 
1423
  # Initialize BrowserState for persistent session management - stores all session data in localStorage
1424
- browser_state = gr.BrowserState(
1425
- {
1426
- "app_session_id": "", # App-generated persistent session ID
1427
- "audio_generation_state": {
1428
- "is_generating": False,
1429
- "progress": 0.0,
1430
- "status": "idle",
1431
- "current_script": "",
1432
- "final_audio_path": None,
1433
- "streaming_parts": [],
1434
- "generation_id": None,
1435
- "start_time": None,
1436
- "estimated_total_parts": 1,
1437
- },
1438
- "user_settings": {
1439
- "current_api_type": "gemini",
1440
- "document_type": "research_paper",
1441
- "podcast_mode": "academic",
1442
- "character1": "Zundamon",
1443
- "character2": "Shikoku Metan",
1444
- "openai_max_tokens": 4000,
1445
- "gemini_max_tokens": 8000,
1446
- "openai_model": "gpt-4o-mini",
1447
- "gemini_model": "gemini-1.5-flash",
1448
- },
1449
- "ui_state": {"podcast_text": "", "terms_agreed": False},
1450
- }
1451
- )
1452
  # Initialize regular State for UserSession object (not serializable to localStorage)
1453
  user_session = gr.State()
1454
 
@@ -1736,13 +1699,8 @@ class PaperPodcastApp:
1736
  outputs=[generate_btn, browser_state],
1737
  )
1738
 
1739
- # extracted_textの変更時にもbrowser_stateを更新
1740
- extracted_text.change(
1741
- fn=self.update_browser_state_extracted_text,
1742
- inputs=[extracted_text, browser_state],
1743
- outputs=[browser_state],
1744
- queue=False,
1745
- )
1746
 
1747
  return app
1748
 
@@ -1962,10 +1920,6 @@ class PaperPodcastApp:
1962
 
1963
  return button_update, updated_browser_state
1964
 
1965
- def update_browser_state_extracted_text(self, extracted_text: str, browser_state: Dict[str, Any]) -> Dict[str, Any]:
1966
- """Update browser state with extracted text changes."""
1967
- return self.update_browser_state_ui_content(browser_state, browser_state.get("podcast_text", ""), browser_state.get("terms_agreed", False))
1968
-
1969
  def set_document_type(self, doc_type: str, user_session: UserSession, browser_state: Dict[str, Any]) -> Tuple[UserSession, Dict[str, Any]]:
1970
  """
1971
  ドキュメントタイプを設定します。
@@ -2120,10 +2074,6 @@ class PaperPodcastApp:
2120
  updated_browser_state["audio_generation_state"]["final_audio_path"] = final_audio
2121
  updated_browser_state["audio_generation_state"]["status"] = "completed"
2122
 
2123
- # Update BrowserState with current session status and UI content
2124
- updated_browser_state = self.update_browser_state_audio_status(user_session, updated_browser_state)
2125
- updated_browser_state = self.update_browser_state_ui_content(updated_browser_state, restored_podcast_text, restored_terms_agreed)
2126
-
2127
  # Step 4: Create UI component updates (enable all components)
2128
  logger.info(f"Enabling UI components for session {user_session.session_id}")
2129
  logger.debug(f"UI sync values: document_type={document_type}, podcast_mode={podcast_mode}, character1={character1}, character2={character2}")
 
64
  # Restore settings from browser state
65
  user_session.update_settings_from_browser_state(browser_state)
66
 
67
+ # Ensure browser_state completeness using user_session defaults
68
+ complete_browser_state = user_session.ensure_browser_state_completeness(browser_state)
69
+
70
+ return user_session, complete_browser_state
71
  else:
72
  # Create new session with UUID-based ID
73
  user_session = UserSession() # Will generate new UUID
74
  logger.info(f"User session initialized: {user_session.session_id}")
75
 
76
+ # Use user_session's default state structure to complete browser_state
77
+ complete_browser_state = user_session.ensure_browser_state_completeness(browser_state)
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ return user_session, complete_browser_state
 
 
 
 
 
80
 
81
  def update_browser_state_ui_content(self, browser_state: Dict[str, Any], podcast_text: str, terms_agreed: bool, extracted_text: str = "") -> Dict[str, Any]:
82
+ """Update BrowserState with UI content for recovery.
83
+
84
+ Note: This method is being phased out in favor of UserSession.ensure_browser_state_completeness.
85
+ Consider using user_session.ensure_browser_state_completeness instead for new code.
86
+ """
87
  updated_state = browser_state.copy()
88
 
89
  # Update ui_state section in the new BrowserState structure
 
1410
  )
1411
 
1412
  # Initialize BrowserState for persistent session management - stores all session data in localStorage
1413
+ # Start with empty dict and let initialize_session_and_ui populate with proper defaults
1414
+ browser_state = gr.BrowserState({})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1415
  # Initialize regular State for UserSession object (not serializable to localStorage)
1416
  user_session = gr.State()
1417
 
 
1699
  outputs=[generate_btn, browser_state],
1700
  )
1701
 
1702
+ # Note: extracted_text changes don't need to update browser_state
1703
+ # as extracted_text is temporary and not persisted
 
 
 
 
 
1704
 
1705
  return app
1706
 
 
1920
 
1921
  return button_update, updated_browser_state
1922
 
 
 
 
 
1923
  def set_document_type(self, doc_type: str, user_session: UserSession, browser_state: Dict[str, Any]) -> Tuple[UserSession, Dict[str, Any]]:
1924
  """
1925
  ドキュメントタイプを設定します。
 
2074
  updated_browser_state["audio_generation_state"]["final_audio_path"] = final_audio
2075
  updated_browser_state["audio_generation_state"]["status"] = "completed"
2076
 
 
 
 
 
2077
  # Step 4: Create UI component updates (enable all components)
2078
  logger.info(f"Enabling UI components for session {user_session.session_id}")
2079
  logger.debug(f"UI sync values: document_type={document_type}, podcast_mode={podcast_mode}, character1={character1}, character2={character2}")
yomitalk/user_session.py CHANGED
@@ -282,6 +282,84 @@ class UserSession:
282
 
283
  return browser_state
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  # Temporary compatibility methods for tests - will be removed
286
  @property
287
  def audio_generation_state(self) -> Dict[str, Any]:
 
282
 
283
  return browser_state
284
 
285
+ def get_default_browser_state_structure(self) -> Dict[str, Any]:
286
+ """Get default browser state structure based on current user session settings.
287
+
288
+ This method provides the single source of truth for default values,
289
+ preventing duplication between user_session and browser_state defaults.
290
+
291
+ Returns:
292
+ Dict[str, Any]: Default browser state structure with current session values
293
+ """
294
+ return {
295
+ "app_session_id": self.session_id,
296
+ "audio_generation_state": {
297
+ "is_generating": False,
298
+ "progress": 0.0,
299
+ "status": "idle",
300
+ "current_script": "",
301
+ "final_audio_path": None,
302
+ "streaming_parts": [],
303
+ "generation_id": None,
304
+ "start_time": None,
305
+ "estimated_total_parts": 1,
306
+ },
307
+ "user_settings": {
308
+ "current_api_type": self.text_processor.current_api_type.name.lower() if self.text_processor.current_api_type else "gemini",
309
+ "document_type": self.text_processor.prompt_manager.current_document_type.value,
310
+ "podcast_mode": self.text_processor.prompt_manager.current_mode.value,
311
+ "character1": self.text_processor.prompt_manager.char_mapping.get("Character1", "Zundamon"),
312
+ "character2": self.text_processor.prompt_manager.char_mapping.get("Character2", "Shikoku Metan"),
313
+ "openai_max_tokens": self.text_processor.openai_model.get_max_tokens(),
314
+ "gemini_max_tokens": self.text_processor.gemini_model.get_max_tokens(),
315
+ "openai_model": self.text_processor.openai_model.model_name,
316
+ "gemini_model": self.text_processor.gemini_model.model_name,
317
+ },
318
+ "ui_state": {"podcast_text": "", "terms_agreed": False},
319
+ }
320
+
321
+ def ensure_browser_state_completeness(self, browser_state: Dict[str, Any]) -> Dict[str, Any]:
322
+ """Ensure browser_state has complete structure using user_session defaults.
323
+
324
+ This method merges incomplete browser_state with user_session-based defaults,
325
+ giving priority to user_session values when browser_state is missing fields.
326
+
327
+ Args:
328
+ browser_state (Dict[str, Any]): Current browser state (might be incomplete)
329
+
330
+ Returns:
331
+ Dict[str, Any]: Complete browser state with user_session defaults applied
332
+ """
333
+ # Get default structure from current user session state
334
+ default_structure = self.get_default_browser_state_structure()
335
+
336
+ # If browser_state is completely empty, use defaults
337
+ if not browser_state:
338
+ return default_structure
339
+
340
+ # Merge browser_state into defaults - this gives priority to user_session defaults
341
+ # for missing fields while preserving existing browser_state values
342
+ result = default_structure.copy()
343
+
344
+ # Recursively merge dictionaries, keeping browser_state values where they exist
345
+ def deep_merge(default_dict: dict, browser_dict: dict) -> dict:
346
+ merged = default_dict.copy()
347
+ for key, value in browser_dict.items():
348
+ if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
349
+ merged[key] = deep_merge(merged[key], value)
350
+ else:
351
+ merged[key] = value
352
+ return merged
353
+
354
+ merged_result = deep_merge(result, browser_state)
355
+
356
+ # Special case: always ensure app_session_id matches current session
357
+ # This handles the case where browser_state has empty session_id
358
+ if not merged_result.get("app_session_id"):
359
+ merged_result["app_session_id"] = self.session_id
360
+
361
+ return merged_result
362
+
363
  # Temporary compatibility methods for tests - will be removed
364
  @property
365
  def audio_generation_state(self) -> Dict[str, Any]: