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 = "
" steps_html = "\n".join( [ f'
' f'' f'{"✅" if completed else spinner_div}' f'' f'{step}' f'
' for step, completed in steps ] ) # status_description = '

Processing steps below.

' status_description = '' if error_text: error_html = f'
{error_text}
' else: error_html = '' return f'''

Status: {status}

{status_description} {error_html}
{steps_html}
''' 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 = ( '' '' '' ) postfix = ( '' f'Effect: {effect_description}' '' '' ) 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'{text}' def _generate_legend_for_text_split_html( character_phrases: list[CharacterPhrase], add_effect_legend: bool = False ) -> str: html = ( "
" "
Legend:
" ) 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"
{character}
" if add_effect_legend: html += ( '
' '🎵 #1' ' - sound effect start position (hover to see the prompt)' '
' ) html += "
" return html def _generate_text_split_html( character_phrases: list[CharacterPhrase], ) -> tuple[str, dict[int, int]]: html_items = ["
"] 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"" suffix = '' # 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("
") 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}
{content_html}
' 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'''
{character} Gender: {voice_properties.get('gender', 'N/A')}, Age: {voice_properties.get('age_group', 'N/A')}, Voice ID: {voice_properties.get('voice_id', 'N/A')}
''' return html AUDIO_PLAYER_CSS = """\ """