Kyosuke Ichikawa commited on
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 +11 -2
- tests/unit/test_ui_initialization.py +14 -52
- yomitalk/app.py +16 -66
- yomitalk/user_session.py +78 -0
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
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 216 |
-
"""Test updating
|
| 217 |
-
from yomitalk.user_session import UserSession
|
| 218 |
-
|
| 219 |
-
user_session = UserSession()
|
| 220 |
browser_state = {
|
| 221 |
-
"app_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
|
| 236 |
-
updated_state = self.app.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
-
#
|
| 272 |
-
assert updated_state
|
| 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 |
-
#
|
| 68 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 75 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 1740 |
-
extracted_text
|
| 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]:
|