Spaces:
				
			
			
	
			
			
					
		Running
		
	
	
	
			
			
	
	
	
	
		
		
					
		Running
		
	| from src.sound_effects_design import SoundEffectDescription | |
| from src.text_split_chain import CharacterPhrase | |
| from src.utils import ( | |
| get_audio_from_voice_id, | |
| get_character_color, | |
| get_collection_safe_index, | |
| hex_to_rgb, | |
| prettify_unknown_character_label, | |
| ) | |
| from src.web.variables import EFFECT_CSS | |
| def create_status_html(status: str, steps: list[tuple[str, bool]], error_text: str = '') -> str: | |
| # CSS for the spinner animation | |
| spinner_css = """ | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .spinner { | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid #e0e0e0; | |
| border-top: 3px solid #3498db; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| display: inline-block; | |
| } | |
| """ | |
| spinner_div = "<div class='spinner'></div>" | |
| steps_html = "\n".join( | |
| [ | |
| f'<div class="step-item" style="display: flex; align-items: center; padding: 0.8rem; margin-bottom: 0.5rem; background-color: #31395294; border-radius: 6px; font-weight: 600;">' | |
| f'<span class="step-icon" style="margin-right: 1rem; font-size: 1.3rem;">' | |
| f'{"β " if completed else spinner_div}' | |
| f'</span>' | |
| f'<span class="step-text" style="font-size: 1.1rem; color: #e0e0e0;">{step}</span>' | |
| f'</div>' | |
| for step, completed in steps | |
| ] | |
| ) | |
| # status_description = '<p class="status-description" style="margin: 0.5rem 0 0 0; color: #c0c0c0; font-size: 1rem; font-weight: 400;">Processing steps below.</p>' | |
| status_description = '' | |
| if error_text: | |
| error_html = f'<div class="error-message" style="color: #e53e3e; font-size: 1.2em;">{error_text}</div></div>' | |
| else: | |
| error_html = '' | |
| return f''' | |
| <div class="status-container" style="font-family: system-ui; max-width: 1472px; margin: 0 auto; background-color: #31395294; padding: 1rem; border-radius: 8px; color: #f0f0f0;"> | |
| <style>{spinner_css}</style> | |
| <div class="status-header" style="background: #31395294; padding: 1rem; border-radius: 8px; font-weight: bold;"> | |
| <h3 class="status-title" style="margin: 0; color: rgb(224, 224, 224); font-size: 1.5rem; font-weight: 700;">Status: {status}</h3> | |
| {status_description} | |
| {error_html} | |
| </div> | |
| <div class="steps" style="margin-top: 1rem;"> | |
| {steps_html} | |
| </div> | |
| </div> | |
| ''' | |
| def create_effect_span_prefix_postfix(effect_description: str): | |
| """Create an HTML span with effect tooltip.""" | |
| # NOTE: it's important not to use multiline python string in order not to add whitespaces | |
| prefix = ( | |
| '<span class="character-segment">' | |
| '<span class="effect-container">' | |
| '<span class="effect-text">' | |
| ) | |
| postfix = ( | |
| '</span>' | |
| f'<span class="effect-tooltip">Effect: {effect_description}</span>' | |
| '</span>' | |
| '</span>' | |
| ) | |
| return prefix, postfix | |
| def create_effect_span(text: str, effect_description: str) -> str: | |
| prefix, postfix = create_effect_span_prefix_postfix(effect_description=effect_description) | |
| res = f"{prefix}{text}{postfix}" | |
| return res | |
| def create_regular_span(text: str, bg_color: str) -> str: | |
| """Create a regular HTML span with background color.""" | |
| return f'<span class="character-segment" style="background-color: {bg_color}">{text}</span>' | |
| def _generate_legend_for_text_split_html( | |
| character_phrases: list[CharacterPhrase], add_effect_legend: bool = False | |
| ) -> str: | |
| html = ( | |
| "<div style='margin-bottom: 1rem;'>" | |
| "<div style='font-size: 1.35em; font-weight: bold;'>Legend:</div>" | |
| ) | |
| unique_characters = set(phrase.character or 'Unassigned' for phrase in character_phrases) | |
| characters_sorted = sorted(unique_characters, key=lambda c: c.lower()) | |
| for character in characters_sorted: | |
| color = get_character_color(character) | |
| html += f"<div style='color: {color}; font-size: 1.1em; margin-bottom: 0.25rem;'>{character}</div>" | |
| if add_effect_legend: | |
| html += ( | |
| '<div style="font-size: 1.1em; margin-bottom: 0.25rem;">' | |
| '<span class="effect-text">π΅ #1</span>' | |
| ' - sound effect start position (hover to see the prompt)' | |
| '</div>' | |
| ) | |
| html += "</div>" | |
| return html | |
| def _generate_text_split_html( | |
| character_phrases: list[CharacterPhrase], | |
| ) -> tuple[str, dict[int, int]]: | |
| html_items = ["<div style='font-size: 1.2em; line-height: 1.6;'>"] | |
| index_mapping = {} # Mapping from original index to HTML index | |
| orig_index = 0 # Index in the original text | |
| html_index = len(html_items[0]) # Index in the HTML output | |
| for phrase in character_phrases: | |
| character = phrase.character or 'Unassigned' | |
| text = phrase.text | |
| color = get_character_color(character) | |
| rgba_color = f"rgba({hex_to_rgb(color)}, 0.5)" | |
| prefix = f"<span style='background-color: {rgba_color}; border-radius: 0.2em;'>" | |
| suffix = '</span>' | |
| # Append the HTML for this phrase | |
| html_items.append(f"{prefix}{text}{suffix}") | |
| # Map each character index from the original text to the HTML text | |
| html_index += len(prefix) | |
| for i in range(len(text)): | |
| index_mapping[orig_index + i] = html_index + i | |
| # Update indices | |
| orig_index += len(text) | |
| html_index += len(text) + len(suffix) | |
| html_items.append("</div>") | |
| html = ''.join(html_items) | |
| return html, index_mapping | |
| def generate_text_split_inner_html_no_effect(character_phrases: list[CharacterPhrase]) -> str: | |
| legend_html = _generate_legend_for_text_split_html( | |
| character_phrases=character_phrases, add_effect_legend=False | |
| ) | |
| text_split_html, char_ix_orig_2_html = _generate_text_split_html( | |
| character_phrases=character_phrases | |
| ) | |
| return legend_html + text_split_html | |
| def generate_text_split_inner_html_with_effects( | |
| character_phrases: list[CharacterPhrase], | |
| sound_effects_descriptions: list[SoundEffectDescription], | |
| ) -> str: | |
| legend_html = _generate_legend_for_text_split_html( | |
| character_phrases=character_phrases, add_effect_legend=True | |
| ) | |
| text_split_html, char_ix_orig_2_html = _generate_text_split_html( | |
| character_phrases=character_phrases | |
| ) | |
| if not sound_effects_descriptions: | |
| return legend_html + text_split_html | |
| prev_end = 0 | |
| content_html_parts = [] | |
| for ix, sed in enumerate(sound_effects_descriptions, start=1): | |
| # NOTE: 'sed' contains approximate indices from the original text. | |
| # that's why we use safe conversion before accessing char mapping | |
| ix_start = get_collection_safe_index( | |
| ix=sed.ix_start_orig_text, collection=char_ix_orig_2_html | |
| ) | |
| # ix_end = get_collection_safe_index(ix=sed.ix_end_orig_text, collection=char_ix_orig_2_html) | |
| html_start_ix = char_ix_orig_2_html[ix_start] | |
| # html_end_ix = char_ix_orig_2_html[ix_end] # NOTE: this is incorrect | |
| # BUG: here we take exact same number of characters as in text between sound effect tags. | |
| # This introduces the bug: HTML text could be included in 'text_under_effect', | |
| # due to inaccuracies in 'sed' indices. | |
| # html_end_ix = html_start_ix + ix_end - ix_start # NOTE: this is correct | |
| # NOTE: reason is that html may exist between original text characters | |
| prefix = text_split_html[prev_end:html_start_ix] | |
| if prefix: | |
| content_html_parts.append(prefix) | |
| # text_under_effect = text_split_html[html_start_ix:html_end_ix] | |
| text_under_effect = f'π΅ #{ix}' | |
| if text_under_effect: | |
| effect_prefix, effect_postfix = create_effect_span_prefix_postfix( | |
| effect_description=sed.prompt | |
| ) | |
| text_under_effect_wrapped = f'{effect_prefix}{text_under_effect}{effect_postfix}' | |
| content_html_parts.append(text_under_effect_wrapped) | |
| # prev_end = html_end_ix | |
| prev_end = html_start_ix | |
| last = text_split_html[prev_end:] | |
| if last: | |
| content_html_parts.append(last) | |
| content_html = ''.join(content_html_parts) | |
| content_html = f'{EFFECT_CSS}<div class="text-effect-container">{content_html}</div>' | |
| html = legend_html + content_html | |
| return html | |
| def generate_voice_mapping_inner_html(select_voice_chain_out): | |
| character2props = {} | |
| html = AUDIO_PLAYER_CSS | |
| for key in set(select_voice_chain_out.character2props) | set( | |
| select_voice_chain_out.character2voice | |
| ): | |
| character_props = select_voice_chain_out.character2props.get(key, []).model_dump() | |
| character_props["voice_id"] = select_voice_chain_out.character2voice.get(key, []) | |
| character_props["sample_audio_url"] = get_audio_from_voice_id(character_props["voice_id"]) | |
| character2props[prettify_unknown_character_label(key)] = character_props | |
| for character, voice_properties in sorted(character2props.items(), key=lambda x: x[0].lower()): | |
| color = get_character_color(character) | |
| audio_url = voice_properties.get('sample_audio_url', '') | |
| html += f''' | |
| <div class="voice-assignment"> | |
| <div class="voice-details"> | |
| <span class="character-name" style="color: {color};">{character}</span> | |
| <span>β</span> | |
| <span class="voice-props"> | |
| Gender: {voice_properties.get('gender', 'N/A')}, | |
| Age: {voice_properties.get('age_group', 'N/A')}, | |
| Voice ID: {voice_properties.get('voice_id', 'N/A')} | |
| </span> | |
| </div> | |
| <div class="custom-audio-player"> | |
| <audio controls preload="none"> | |
| <source src="{audio_url}" type="audio/mpeg"> | |
| Your browser does not support the audio element. | |
| </audio> | |
| </div> | |
| </div> | |
| ''' | |
| return html | |
| AUDIO_PLAYER_CSS = """\ | |
| <style> | |
| .custom-audio-player { | |
| display: inline-block; | |
| width: 250px; | |
| --bg-color: #ff79c6; | |
| --highlight-color: #4299e100; | |
| --text-color: #e0e0e0; | |
| --border-radius: 0px; | |
| } | |
| .custom-audio-player audio { | |
| width: 100%; | |
| height: 36px; | |
| border-radius: var(--border-radius); | |
| background-color: #3f2a2a00; | |
| outline: none; | |
| } | |
| .custom-audio-player audio::-webkit-media-controls-panel { | |
| background-color: var(--bg-color); | |
| } | |
| .custom-audio-player audio::-webkit-media-controls-current-time-display, | |
| .custom-audio-player audio::-webkit-media-controls-time-remaining-display { | |
| color: var(--text-color); | |
| } | |
| .custom-audio-player audio::-webkit-media-controls-play-button { | |
| background-color: var(--highlight-color); | |
| border-radius: 50%; | |
| height: 30px; | |
| width: 30px; | |
| } | |
| .custom-audio-player audio::-webkit-media-controls-timeline { | |
| background-color: var(--bg-color); | |
| height: 6px; | |
| border-radius: 3px; | |
| } | |
| /* Container styles for voice assignment display */ | |
| .voice-assignment { | |
| background-color: rgba(49, 57, 82, 0.8); | |
| padding: 1rem; | |
| padding-left: 1rem; | |
| padding-right: 1rem; | |
| padding-top: 0.2rem; | |
| padding-bottom: 0.2rem; | |
| border-radius: var(--border-radius); | |
| margin-top: 0.5rem; | |
| color: var(--text-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| border-radius: 7px; | |
| } | |
| .voice-assignment span { | |
| font-weight: 600; | |
| } | |
| .voice-details { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .character-name { | |
| color: var(--highlight-color); | |
| font-weight: bold; | |
| } | |
| .voice-props { | |
| color: #4a5568; | |
| } | |
| </style> | |
| """ | |