nihun commited on
Commit
9258a34
ยท
verified ยท
1 Parent(s): 9bf43d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +821 -606
app.py CHANGED
@@ -1,607 +1,822 @@
1
- """
2
- ๐ŸŽŒ Anime Translator with Lip-Sync
3
- =================================
4
-
5
- A Streamlit application that translates text between English and Hindi,
6
- converts it to speech, and generates a lip-synced anime avatar animation.
7
- """
8
-
9
- import streamlit as st
10
- from pathlib import Path
11
- import tempfile
12
- import time
13
- import shutil
14
- import os
15
- import subprocess
16
- from shutil import which
17
- from typing import Tuple, Optional
18
-
19
- # Import utility modules
20
- from utils.translator import translate_text, detect_language
21
- from utils.tts_engine import synthesize_speech, get_audio_duration
22
- from utils.lipsync import generate_lipsync_gif
23
- from utils.speech_to_text import transcribe_audio, get_language_code
24
- from utils.avatar_manager import list_avatars, get_avatar_preview, ensure_sample_avatar
25
-
26
- # =============================================================================
27
- # FFmpeg Configuration
28
- # =============================================================================
29
-
30
- def configure_ffmpeg():
31
- """Configure FFmpeg path for pydub on Windows."""
32
- possible_paths = [
33
- r"C:\ffmpeg\bin",
34
- r"C:\Program Files\ffmpeg\bin",
35
- r"C:\Program Files (x86)\ffmpeg\bin",
36
- os.path.expanduser("~\\ffmpeg\\bin"),
37
- r"C:\Users\Nishant Pratap\ffmpeg\bin", # Add your user-specific path
38
- ]
39
-
40
- if which("ffmpeg") is not None:
41
- return True
42
-
43
- for path in possible_paths:
44
- ffmpeg_exe = os.path.join(path, "ffmpeg.exe")
45
- if os.path.exists(ffmpeg_exe):
46
- os.environ["PATH"] = path + os.pathsep + os.environ.get("PATH", "")
47
- # Also set for pydub specifically
48
- try:
49
- from pydub import AudioSegment
50
- AudioSegment.converter = os.path.join(path, "ffmpeg.exe")
51
- AudioSegment.ffprobe = os.path.join(path, "ffprobe.exe")
52
- except:
53
- pass
54
- return True
55
-
56
- return False
57
-
58
-
59
- def check_ffmpeg_detailed():
60
- """Check FFmpeg installation and return detailed status."""
61
- status = {
62
- "ffmpeg_in_path": False,
63
- "ffmpeg_works": False,
64
- "ffprobe_works": False,
65
- "pydub_works": False,
66
- "error_message": None
67
- }
68
-
69
- ffmpeg_path = which("ffmpeg")
70
- status["ffmpeg_in_path"] = ffmpeg_path is not None
71
-
72
- try:
73
- result = subprocess.run(
74
- ["ffmpeg", "-version"],
75
- capture_output=True,
76
- text=True,
77
- timeout=5
78
- )
79
- status["ffmpeg_works"] = result.returncode == 0
80
- except Exception as e:
81
- status["error_message"] = str(e)
82
-
83
- try:
84
- result = subprocess.run(
85
- ["ffprobe", "-version"],
86
- capture_output=True,
87
- text=True,
88
- timeout=5
89
- )
90
- status["ffprobe_works"] = result.returncode == 0
91
- except Exception:
92
- pass
93
-
94
- try:
95
- from pydub import AudioSegment
96
- silence = AudioSegment.silent(duration=100)
97
- status["pydub_works"] = True
98
- except Exception as e:
99
- status["pydub_works"] = False
100
- if not status["error_message"]:
101
- status["error_message"] = str(e)
102
-
103
- return status
104
-
105
-
106
- ffmpeg_found = configure_ffmpeg()
107
-
108
- # =============================================================================
109
- # Configuration
110
- # =============================================================================
111
-
112
- AVATARS_DIR = Path("./avatars")
113
- TEMP_DIR = Path(tempfile.gettempdir()) / "anime_translator"
114
-
115
- AVATARS_DIR.mkdir(parents=True, exist_ok=True)
116
- TEMP_DIR.mkdir(parents=True, exist_ok=True)
117
-
118
- # Page configuration
119
- st.set_page_config(
120
- page_title="๐ŸŽŒ Anime Translator",
121
- page_icon="๐ŸŽŒ",
122
- layout="wide",
123
- initial_sidebar_state="expanded"
124
- )
125
-
126
- # =============================================================================
127
- # Custom CSS Styling
128
- # =============================================================================
129
-
130
- st.markdown("""
131
- <style>
132
- .main {
133
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
134
- }
135
-
136
- .main-header {
137
- background: linear-gradient(90deg, #e94560, #ff6b6b);
138
- -webkit-background-clip: text;
139
- -webkit-text-fill-color: transparent;
140
- font-size: 3rem;
141
- font-weight: bold;
142
- text-align: center;
143
- padding: 1rem;
144
- margin-bottom: 2rem;
145
- }
146
-
147
- .stButton > button {
148
- background: linear-gradient(90deg, #e94560, #ff6b6b);
149
- color: white;
150
- border: none;
151
- border-radius: 25px;
152
- padding: 0.75rem 2rem;
153
- font-weight: bold;
154
- transition: all 0.3s ease;
155
- width: 100%;
156
- }
157
-
158
- .stButton > button:hover {
159
- transform: translateY(-2px);
160
- box-shadow: 0 5px 20px rgba(233, 69, 96, 0.4);
161
- }
162
-
163
- .result-box {
164
- background: linear-gradient(135deg, rgba(233, 69, 96, 0.1), rgba(255, 107, 107, 0.1));
165
- border-radius: 15px;
166
- padding: 1.5rem;
167
- border: 1px solid rgba(233, 69, 96, 0.3);
168
- margin: 1rem 0;
169
- }
170
-
171
- .info-box {
172
- background: rgba(100, 200, 255, 0.1);
173
- border-left: 4px solid #64c8ff;
174
- padding: 1rem;
175
- border-radius: 0 10px 10px 0;
176
- margin: 1rem 0;
177
- }
178
-
179
- .success-box {
180
- background: rgba(100, 255, 150, 0.1);
181
- border-left: 4px solid #64ff96;
182
- padding: 1rem;
183
- border-radius: 0 10px 10px 0;
184
- }
185
-
186
- #MainMenu {visibility: hidden;}
187
- footer {visibility: hidden;}
188
-
189
- .stTabs [data-baseweb="tab-list"] {
190
- gap: 8px;
191
- }
192
-
193
- .stTabs [data-baseweb="tab"] {
194
- background: rgba(255, 255, 255, 0.05);
195
- border-radius: 10px;
196
- padding: 10px 20px;
197
- }
198
-
199
- .stTabs [aria-selected="true"] {
200
- background: linear-gradient(90deg, #e94560, #ff6b6b);
201
- }
202
- </style>
203
- """, unsafe_allow_html=True)
204
-
205
- # =============================================================================
206
- # Helper Functions
207
- # =============================================================================
208
-
209
- def cleanup_temp_files(older_than_sec: int = 3600) -> None:
210
- """Clean up old temporary files."""
211
- now = time.time()
212
- try:
213
- for path in TEMP_DIR.iterdir():
214
- try:
215
- if now - path.stat().st_mtime > older_than_sec:
216
- if path.is_file():
217
- path.unlink()
218
- elif path.is_dir():
219
- shutil.rmtree(path)
220
- except Exception:
221
- pass
222
- except Exception:
223
- pass
224
-
225
-
226
- def process_translation_pipeline(
227
- text: str,
228
- source_lang: str,
229
- target_lang: str,
230
- avatar_name: str
231
- ) -> Tuple[str, Optional[str], Optional[str]]:
232
- """Main processing pipeline: translate, synthesize speech, generate animation."""
233
-
234
- # Step 1: Translate text
235
- try:
236
- translated_text = translate_text(text, source_lang, target_lang)
237
- except Exception as e:
238
- raise Exception(f"Translation failed: {str(e)}")
239
-
240
- # Step 2: Synthesize speech
241
- try:
242
- audio_path = synthesize_speech(translated_text, target_lang, TEMP_DIR)
243
- except Exception as e:
244
- raise Exception(f"Speech synthesis failed: {str(e)}")
245
-
246
- # Step 3: Generate lip-sync animation
247
- gif_path = None
248
- try:
249
- gif_path = generate_lipsync_gif(
250
- avatar_name=avatar_name,
251
- audio_path=audio_path,
252
- avatars_dir=AVATARS_DIR,
253
- output_dir=TEMP_DIR,
254
- fps=12
255
- )
256
- except Exception as e:
257
- # Don't fail completely if animation fails
258
- print(f"Animation generation warning: {str(e)}")
259
- gif_path = None
260
-
261
- return translated_text, audio_path, gif_path
262
-
263
-
264
- # =============================================================================
265
- # Sidebar
266
- # =============================================================================
267
-
268
- with st.sidebar:
269
- st.markdown("## โš™๏ธ Settings")
270
-
271
- # Avatar selection
272
- st.markdown("### ๐ŸŽญ Avatar Selection")
273
- avatars = list_avatars(AVATARS_DIR)
274
-
275
- if avatars:
276
- selected_avatar = st.selectbox(
277
- "Choose your avatar",
278
- options=avatars,
279
- index=0,
280
- help="Select an anime avatar for lip-sync animation"
281
- )
282
-
283
- preview = get_avatar_preview(selected_avatar, AVATARS_DIR)
284
- if preview:
285
- st.image(preview, caption=f"Preview: {selected_avatar}", width="stretch")
286
- else:
287
- st.warning("No avatars found. Creating sample avatar...")
288
- ensure_sample_avatar(AVATARS_DIR)
289
- selected_avatar = "sample"
290
- st.rerun()
291
-
292
- st.markdown("---")
293
-
294
- # Language settings
295
- st.markdown("### ๐ŸŒ Language Settings")
296
-
297
- source_language = st.selectbox(
298
- "Source Language",
299
- options=["auto", "en", "hi"],
300
- format_func=lambda x: {"auto": "๐Ÿ”„ Auto-detect", "en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x],
301
- index=0
302
- )
303
-
304
- target_language = st.selectbox(
305
- "Target Language",
306
- options=["en", "hi"],
307
- format_func=lambda x: {"en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x],
308
- index=1
309
- )
310
-
311
- st.markdown("---")
312
-
313
- # System status
314
- st.markdown("### ๐Ÿ”ง System Status")
315
-
316
- ffmpeg_status = check_ffmpeg_detailed()
317
-
318
- if ffmpeg_status["ffmpeg_works"]:
319
- st.success("โœ… FFmpeg: Working")
320
- else:
321
- st.error("โŒ FFmpeg: Not working")
322
-
323
- if ffmpeg_status["pydub_works"]:
324
- st.success("โœ… Pydub: Working")
325
- else:
326
- st.warning("โš ๏ธ Pydub: Limited (fallback mode)")
327
-
328
- if ffmpeg_status["error_message"]:
329
- with st.expander("๐Ÿ” Error Details"):
330
- st.code(ffmpeg_status["error_message"])
331
- st.markdown("""
332
- **To fix FFmpeg:**
333
- ```bash
334
- conda install -c conda-forge ffmpeg
335
- ```
336
- Or download from: https://www.gyan.dev/ffmpeg/builds/
337
- """)
338
-
339
- st.markdown("---")
340
-
341
- # Info section
342
- st.markdown("### โ„น๏ธ About")
343
- st.markdown("""
344
- Translate text between English and Hindi with lip-synced avatar animation.
345
-
346
- **Features:**
347
- - ๐ŸŽค Voice input
348
- - ๐Ÿ”„ Auto detection
349
- - ๐Ÿ—ฃ๏ธ Text-to-speech
350
- - ๐ŸŽฌ Lip-sync animation
351
- """)
352
-
353
- if st.button("๐Ÿงน Clear Temp Files"):
354
- cleanup_temp_files(older_than_sec=0)
355
- st.success("Cleared!")
356
-
357
-
358
- # =============================================================================
359
- # Main Content
360
- # =============================================================================
361
-
362
- st.markdown('<h1 class="main-header">๐ŸŽŒ Anime Translator</h1>', unsafe_allow_html=True)
363
- st.markdown(
364
- '<p style="text-align: center; color: #888; font-size: 1.2rem;">'
365
- 'Translate โ€ข Speak โ€ข Animate</p>',
366
- unsafe_allow_html=True
367
- )
368
-
369
- # Tabs
370
- tab1, tab2 = st.tabs(["๐Ÿ“ Text Input", "๐ŸŽค Voice Input"])
371
-
372
- # =============================================================================
373
- # Tab 1: Text Input
374
- # =============================================================================
375
-
376
- with tab1:
377
- col1, col2 = st.columns([1, 1])
378
-
379
- with col1:
380
- st.markdown("### ๐Ÿ“ Enter Your Text")
381
-
382
- text_input = st.text_area(
383
- "Type or paste your text here",
384
- height=150,
385
- placeholder="Enter text in English or Hindi...\nเค‰เคฆเคพเคนเคฐเคฃ: เคจเคฎเคธเฅเคคเฅ‡, เค†เคช เค•เฅˆเคธเฅ‡ เคนเฅˆเค‚?\nExample: Hello, how are you?",
386
- key="text_input"
387
- )
388
-
389
- if text_input:
390
- detected = detect_language(text_input)
391
- st.markdown(
392
- f'<div class="info-box">'
393
- f'๐Ÿ“Š Characters: {len(text_input)} | '
394
- f'๐Ÿ” Detected: {"๐Ÿ‡ฎ๐Ÿ‡ณ Hindi" if detected == "hi" else "๐Ÿ‡ฌ๐Ÿ‡ง English"}'
395
- f'</div>',
396
- unsafe_allow_html=True
397
- )
398
-
399
- translate_btn = st.button(
400
- "๐Ÿš€ Translate & Animate",
401
- key="translate_text_btn",
402
- use_container_width=True
403
- )
404
-
405
- with col2:
406
- st.markdown("### ๐ŸŽฌ Result")
407
-
408
- if translate_btn and text_input:
409
- with st.spinner("๐Ÿ”„ Processing..."):
410
- progress = st.progress(0)
411
- status_text = st.empty()
412
-
413
- try:
414
- status_text.text("๐Ÿ“ Translating...")
415
- progress.progress(33)
416
-
417
- translated, audio_path, gif_path = process_translation_pipeline(
418
- text_input,
419
- source_language,
420
- target_language,
421
- selected_avatar
422
- )
423
-
424
- status_text.text("๐Ÿ—ฃ๏ธ Generating speech...")
425
- progress.progress(66)
426
-
427
- status_text.text("๐ŸŽฌ Creating animation...")
428
- progress.progress(100)
429
-
430
- progress.empty()
431
- status_text.empty()
432
-
433
- # Display translated text
434
- st.markdown(
435
- f'<div class="result-box">'
436
- f'<h4>๐Ÿ“œ Translated Text:</h4>'
437
- f'<p style="font-size: 1.2rem;">{translated}</p>'
438
- f'</div>',
439
- unsafe_allow_html=True
440
- )
441
-
442
- # Audio player
443
- if audio_path and os.path.exists(audio_path):
444
- st.markdown("#### ๐Ÿ”Š Audio")
445
- st.audio(audio_path, format="audio/mp3")
446
-
447
- # Animation display
448
- if gif_path and os.path.exists(gif_path):
449
- st.markdown("#### ๐ŸŽญ Lip-Sync Animation")
450
- st.image(gif_path, width="stretch")
451
-
452
- with open(gif_path, "rb") as f:
453
- st.download_button(
454
- label="๐Ÿ“ฅ Download Animation",
455
- data=f,
456
- file_name="lipsync_animation.gif",
457
- mime="image/gif"
458
- )
459
- else:
460
- st.info("โ„น๏ธ Animation not available (FFmpeg may be missing)")
461
-
462
- except Exception as e:
463
- progress.empty()
464
- status_text.empty()
465
- st.error(f"โŒ Error: {str(e)}")
466
-
467
- elif translate_btn:
468
- st.warning("โš ๏ธ Please enter some text to translate.")
469
-
470
-
471
- # =============================================================================
472
- # Tab 2: Voice Input
473
- # =============================================================================
474
-
475
- with tab2:
476
- col1, col2 = st.columns([1, 1])
477
-
478
- with col1:
479
- st.markdown("### ๐ŸŽค Voice Recording")
480
-
481
- st.markdown("""
482
- <div class="info-box">
483
- <strong>Instructions:</strong><br>
484
- 1. Upload an audio file (WAV, MP3, etc.)<br>
485
- 2. Or use the audio recorder below<br>
486
- 3. Click "Transcribe & Translate"
487
- </div>
488
- """, unsafe_allow_html=True)
489
-
490
- uploaded_audio = st.file_uploader(
491
- "Upload an audio file",
492
- type=["wav", "mp3", "ogg", "flac", "m4a"],
493
- help="Supported formats: WAV, MP3, OGG, FLAC, M4A"
494
- )
495
-
496
- recorded_audio = None
497
- try:
498
- from audio_recorder_streamlit import audio_recorder
499
- st.markdown("**Or record directly:**")
500
- recorded_audio = audio_recorder(
501
- text="๐ŸŽ™๏ธ Click to record",
502
- recording_color="#e94560",
503
- neutral_color="#6c757d",
504
- icon_name="microphone",
505
- icon_size="2x"
506
- )
507
- except ImportError:
508
- st.info("๐Ÿ’ก For recording: `pip install audio-recorder-streamlit`")
509
-
510
- voice_lang = st.selectbox(
511
- "Recording Language",
512
- options=["en", "hi"],
513
- format_func=lambda x: {"en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x]
514
- )
515
-
516
- voice_btn = st.button(
517
- "๐ŸŽฏ Transcribe & Translate",
518
- key="voice_btn",
519
- use_container_width=True
520
- )
521
-
522
- with col2:
523
- st.markdown("### ๐ŸŽฌ Result")
524
-
525
- audio_to_process = None
526
-
527
- if uploaded_audio is not None:
528
- temp_audio_path = TEMP_DIR / f"uploaded_{int(time.time()*1000)}.wav"
529
- with open(temp_audio_path, "wb") as f:
530
- f.write(uploaded_audio.getbuffer())
531
- audio_to_process = str(temp_audio_path)
532
- st.audio(uploaded_audio)
533
-
534
- elif recorded_audio is not None:
535
- temp_audio_path = TEMP_DIR / f"recorded_{int(time.time()*1000)}.wav"
536
- with open(temp_audio_path, "wb") as f:
537
- f.write(recorded_audio)
538
- audio_to_process = str(temp_audio_path)
539
- st.audio(recorded_audio, format="audio/wav")
540
-
541
- if voice_btn:
542
- if audio_to_process:
543
- with st.spinner("๐Ÿ”„ Processing voice..."):
544
- try:
545
- st.text("๐ŸŽค Transcribing...")
546
- lang_code = get_language_code(voice_lang)
547
- transcribed_text, success = transcribe_audio(audio_to_process, lang_code)
548
-
549
- if success:
550
- st.markdown(
551
- f'<div class="success-box">'
552
- f'<strong>๐Ÿ“ Transcribed:</strong> {transcribed_text}'
553
- f'</div>',
554
- unsafe_allow_html=True
555
- )
556
-
557
- translated, audio_path, gif_path = process_translation_pipeline(
558
- transcribed_text,
559
- voice_lang,
560
- target_language,
561
- selected_avatar
562
- )
563
-
564
- st.markdown(
565
- f'<div class="result-box">'
566
- f'<h4>๐Ÿ“œ Translated:</h4>'
567
- f'<p style="font-size: 1.2rem;">{translated}</p>'
568
- f'</div>',
569
- unsafe_allow_html=True
570
- )
571
-
572
- if audio_path and os.path.exists(audio_path):
573
- st.markdown("#### ๐Ÿ”Š Audio")
574
- st.audio(audio_path, format="audio/mp3")
575
-
576
- if gif_path and os.path.exists(gif_path):
577
- st.markdown("#### ๐ŸŽญ Animation")
578
- st.image(gif_path, width="stretch")
579
-
580
- with open(gif_path, "rb") as f:
581
- st.download_button(
582
- label="๐Ÿ“ฅ Download",
583
- data=f,
584
- file_name="lipsync.gif",
585
- mime="image/gif"
586
- )
587
- else:
588
- st.error(f"โŒ {transcribed_text}")
589
- except Exception as e:
590
- st.error(f"โŒ Error: {str(e)}")
591
- else:
592
- st.warning("โš ๏ธ Please upload or record audio first.")
593
-
594
-
595
- # =============================================================================
596
- # Footer
597
- # =============================================================================
598
-
599
- st.markdown("---")
600
- st.markdown(
601
- """
602
- <div style="text-align: center; color: #666; padding: 1rem;">
603
- <p>Made By Praveen</p>
604
- </div>
605
- """,
606
- unsafe_allow_html=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  )
 
1
+ """
2
+ ๐ŸŽŒ Anime Translator with Lip-Sync
3
+ =================================
4
+
5
+ A Streamlit application that translates text between English and Hindi,
6
+ converts it to speech, and generates a lip-synced anime avatar animation.
7
+ """
8
+
9
+ import streamlit as st
10
+ from pathlib import Path
11
+ import tempfile
12
+ import time
13
+ import shutil
14
+ import os
15
+ import subprocess
16
+ from shutil import which
17
+ from typing import Tuple, Optional
18
+ import base64
19
+
20
+ # Import utility modules
21
+ from utils.translator import translate_text, detect_language
22
+ from utils.tts_engine import synthesize_speech, get_audio_duration
23
+ from utils.lipsync import generate_lipsync_gif
24
+ from utils.speech_to_text import transcribe_audio, get_language_code
25
+ from utils.avatar_manager import list_avatars, get_avatar_preview, ensure_sample_avatar
26
+
27
+ # =============================================================================
28
+ # FFmpeg Configuration
29
+ # =============================================================================
30
+
31
+ def configure_ffmpeg():
32
+ """Configure FFmpeg path for pydub on Windows."""
33
+ possible_paths = [
34
+ r"C:\ffmpeg\bin",
35
+ r"C:\Program Files\ffmpeg\bin",
36
+ r"C:\Program Files (x86)\ffmpeg\bin",
37
+ os.path.expanduser("~\\ffmpeg\\bin"),
38
+ r"C:\Users\Nishant Pratap\ffmpeg\bin",
39
+ ]
40
+
41
+ if which("ffmpeg") is not None:
42
+ return True
43
+
44
+ for path in possible_paths:
45
+ ffmpeg_exe = os.path.join(path, "ffmpeg.exe")
46
+ if os.path.exists(ffmpeg_exe):
47
+ os.environ["PATH"] = path + os.pathsep + os.environ.get("PATH", "")
48
+ try:
49
+ from pydub import AudioSegment
50
+ AudioSegment.converter = os.path.join(path, "ffmpeg.exe")
51
+ AudioSegment.ffprobe = os.path.join(path, "ffprobe.exe")
52
+ except:
53
+ pass
54
+ return True
55
+
56
+ return False
57
+
58
+
59
+ def check_ffmpeg_detailed():
60
+ """Check FFmpeg installation and return detailed status."""
61
+ status = {
62
+ "ffmpeg_in_path": False,
63
+ "ffmpeg_works": False,
64
+ "ffprobe_works": False,
65
+ "pydub_works": False,
66
+ "error_message": None
67
+ }
68
+
69
+ ffmpeg_path = which("ffmpeg")
70
+ status["ffmpeg_in_path"] = ffmpeg_path is not None
71
+
72
+ try:
73
+ result = subprocess.run(
74
+ ["ffmpeg", "-version"],
75
+ capture_output=True,
76
+ text=True,
77
+ timeout=5
78
+ )
79
+ status["ffmpeg_works"] = result.returncode == 0
80
+ except Exception as e:
81
+ status["error_message"] = str(e)
82
+
83
+ try:
84
+ result = subprocess.run(
85
+ ["ffprobe", "-version"],
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=5
89
+ )
90
+ status["ffprobe_works"] = result.returncode == 0
91
+ except Exception:
92
+ pass
93
+
94
+ try:
95
+ from pydub import AudioSegment
96
+ silence = AudioSegment.silent(duration=100)
97
+ status["pydub_works"] = True
98
+ except Exception as e:
99
+ status["pydub_works"] = False
100
+ if not status["error_message"]:
101
+ status["error_message"] = str(e)
102
+
103
+ return status
104
+
105
+
106
+ ffmpeg_found = configure_ffmpeg()
107
+
108
+ # =============================================================================
109
+ # Configuration
110
+ # =============================================================================
111
+
112
+ AVATARS_DIR = Path("./avatars")
113
+ TEMP_DIR = Path(tempfile.gettempdir()) / "anime_translator"
114
+
115
+ AVATARS_DIR.mkdir(parents=True, exist_ok=True)
116
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
117
+
118
+ # Page configuration
119
+ st.set_page_config(
120
+ page_title="๐ŸŽŒ Anime Translator",
121
+ page_icon="๐ŸŽŒ",
122
+ layout="wide",
123
+ initial_sidebar_state="expanded"
124
+ )
125
+
126
+ # Initialize session state for animation control
127
+ if 'animation_playing' not in st.session_state:
128
+ st.session_state.animation_playing = True
129
+ if 'current_gif_path' not in st.session_state:
130
+ st.session_state.current_gif_path = None
131
+
132
+ # =============================================================================
133
+ # Custom CSS Styling - UPDATED WITH ANIMATION FIX
134
+ # =============================================================================
135
+
136
+ st.markdown("""
137
+ <style>
138
+ .main {
139
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
140
+ }
141
+
142
+ .main-header {
143
+ background: linear-gradient(90deg, #e94560, #ff6b6b);
144
+ -webkit-background-clip: text;
145
+ -webkit-text-fill-color: transparent;
146
+ font-size: 3rem;
147
+ font-weight: bold;
148
+ text-align: center;
149
+ padding: 1rem;
150
+ margin-bottom: 2rem;
151
+ }
152
+
153
+ .stButton > button {
154
+ background: linear-gradient(90deg, #e94560, #ff6b6b);
155
+ color: white;
156
+ border: none;
157
+ border-radius: 25px;
158
+ padding: 0.75rem 2rem;
159
+ font-weight: bold;
160
+ transition: all 0.3s ease;
161
+ width: 100%;
162
+ }
163
+
164
+ .stButton > button:hover {
165
+ transform: translateY(-2px);
166
+ box-shadow: 0 5px 20px rgba(233, 69, 96, 0.4);
167
+ }
168
+
169
+ .result-box {
170
+ background: linear-gradient(135deg, rgba(233, 69, 96, 0.1), rgba(255, 107, 107, 0.1));
171
+ border-radius: 15px;
172
+ padding: 1.5rem;
173
+ border: 1px solid rgba(233, 69, 96, 0.3);
174
+ margin: 1rem 0;
175
+ }
176
+
177
+ .info-box {
178
+ background: rgba(100, 200, 255, 0.1);
179
+ border-left: 4px solid #64c8ff;
180
+ padding: 1rem;
181
+ border-radius: 0 10px 10px 0;
182
+ margin: 1rem 0;
183
+ }
184
+
185
+ .success-box {
186
+ background: rgba(100, 255, 150, 0.1);
187
+ border-left: 4px solid #64ff96;
188
+ padding: 1rem;
189
+ border-radius: 0 10px 10px 0;
190
+ }
191
+
192
+ #MainMenu {visibility: hidden;}
193
+ footer {visibility: hidden;}
194
+
195
+ .stTabs [data-baseweb="tab-list"] {
196
+ gap: 8px;
197
+ }
198
+
199
+ .stTabs [data-baseweb="tab"] {
200
+ background: rgba(255, 255, 255, 0.05);
201
+ border-radius: 10px;
202
+ padding: 10px 20px;
203
+ }
204
+
205
+ .stTabs [aria-selected="true"] {
206
+ background: linear-gradient(90deg, #e94560, #ff6b6b);
207
+ }
208
+
209
+ /* ============================================= */
210
+ /* ANIMATION CONTAINER - FIXED SIZE */
211
+ /* ============================================= */
212
+
213
+ .animation-container {
214
+ width: 100%;
215
+ max-width: 400px;
216
+ height: 400px;
217
+ margin: 0 auto;
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ background: rgba(0, 0, 0, 0.2);
222
+ border-radius: 15px;
223
+ overflow: hidden;
224
+ position: relative;
225
+ }
226
+
227
+ .animation-container img {
228
+ max-width: 100%;
229
+ max-height: 100%;
230
+ object-fit: contain;
231
+ }
232
+
233
+ .animation-container.paused img {
234
+ animation-play-state: paused !important;
235
+ }
236
+
237
+ /* Static image when paused */
238
+ .animation-static {
239
+ width: 100%;
240
+ max-width: 400px;
241
+ height: 400px;
242
+ margin: 0 auto;
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ background: rgba(0, 0, 0, 0.2);
247
+ border-radius: 15px;
248
+ overflow: hidden;
249
+ }
250
+
251
+ .animation-static img {
252
+ max-width: 100%;
253
+ max-height: 100%;
254
+ object-fit: contain;
255
+ }
256
+
257
+ /* Animation controls */
258
+ .animation-controls {
259
+ display: flex;
260
+ justify-content: center;
261
+ gap: 10px;
262
+ margin-top: 10px;
263
+ }
264
+
265
+ .control-btn {
266
+ background: linear-gradient(90deg, #e94560, #ff6b6b);
267
+ color: white;
268
+ border: none;
269
+ border-radius: 20px;
270
+ padding: 8px 20px;
271
+ cursor: pointer;
272
+ font-weight: bold;
273
+ transition: all 0.3s ease;
274
+ }
275
+
276
+ .control-btn:hover {
277
+ transform: scale(1.05);
278
+ box-shadow: 0 3px 15px rgba(233, 69, 96, 0.4);
279
+ }
280
+
281
+ .control-btn.stop {
282
+ background: linear-gradient(90deg, #666, #888);
283
+ }
284
+
285
+ /* Fixed height result column */
286
+ .result-column {
287
+ min-height: 600px;
288
+ }
289
+ </style>
290
+ """, unsafe_allow_html=True)
291
+
292
+ # =============================================================================
293
+ # Helper Functions
294
+ # =============================================================================
295
+
296
+ def cleanup_temp_files(older_than_sec: int = 3600) -> None:
297
+ """Clean up old temporary files."""
298
+ now = time.time()
299
+ try:
300
+ for path in TEMP_DIR.iterdir():
301
+ try:
302
+ if now - path.stat().st_mtime > older_than_sec:
303
+ if path.is_file():
304
+ path.unlink()
305
+ elif path.is_dir():
306
+ shutil.rmtree(path)
307
+ except Exception:
308
+ pass
309
+ except Exception:
310
+ pass
311
+
312
+
313
+ def get_gif_first_frame(gif_path: str) -> Optional[str]:
314
+ """Extract the first frame of a GIF as a static image."""
315
+ try:
316
+ from PIL import Image
317
+ import io
318
+
319
+ with Image.open(gif_path) as img:
320
+ # Get first frame
321
+ img.seek(0)
322
+ first_frame = img.copy()
323
+
324
+ # Save to bytes
325
+ buffer = io.BytesIO()
326
+ first_frame.save(buffer, format='PNG')
327
+ buffer.seek(0)
328
+
329
+ # Convert to base64
330
+ img_base64 = base64.b64encode(buffer.getvalue()).decode()
331
+ return img_base64
332
+ except Exception as e:
333
+ print(f"Error extracting first frame: {e}")
334
+ return None
335
+
336
+
337
+ def display_animation_with_controls(gif_path: str, key_prefix: str = ""):
338
+ """Display animation with play/pause/stop controls."""
339
+
340
+ if not gif_path or not os.path.exists(gif_path):
341
+ st.info("โ„น๏ธ No animation available")
342
+ return
343
+
344
+ # Read GIF file
345
+ with open(gif_path, "rb") as f:
346
+ gif_data = f.read()
347
+ gif_base64 = base64.b64encode(gif_data).decode()
348
+
349
+ # Get first frame for static display
350
+ first_frame_base64 = get_gif_first_frame(gif_path)
351
+
352
+ # Animation state key
353
+ state_key = f"{key_prefix}_playing"
354
+ if state_key not in st.session_state:
355
+ st.session_state[state_key] = True
356
+
357
+ # Control buttons
358
+ col1, col2, col3 = st.columns([1, 1, 1])
359
+
360
+ with col1:
361
+ if st.button("โ–ถ๏ธ Play", key=f"{key_prefix}_play", use_container_width=True):
362
+ st.session_state[state_key] = True
363
+ st.rerun()
364
+
365
+ with col2:
366
+ if st.button("โธ๏ธ Pause", key=f"{key_prefix}_pause", use_container_width=True):
367
+ st.session_state[state_key] = False
368
+ st.rerun()
369
+
370
+ with col3:
371
+ if st.button("โน๏ธ Stop", key=f"{key_prefix}_stop", use_container_width=True):
372
+ st.session_state[state_key] = False
373
+ st.rerun()
374
+
375
+ # Display animation or static frame
376
+ if st.session_state[state_key]:
377
+ # Playing - show animated GIF
378
+ st.markdown(
379
+ f'''
380
+ <div class="animation-container">
381
+ <img src="data:image/gif;base64,{gif_base64}" alt="Lip-sync animation">
382
+ </div>
383
+ ''',
384
+ unsafe_allow_html=True
385
+ )
386
+ else:
387
+ # Paused/Stopped - show first frame
388
+ if first_frame_base64:
389
+ st.markdown(
390
+ f'''
391
+ <div class="animation-static">
392
+ <img src="data:image/png;base64,{first_frame_base64}" alt="Animation paused">
393
+ </div>
394
+ <p style="text-align: center; color: #888; margin-top: 10px;">โธ๏ธ Animation Paused</p>
395
+ ''',
396
+ unsafe_allow_html=True
397
+ )
398
+ else:
399
+ st.info("Animation paused")
400
+
401
+ # Download button
402
+ st.download_button(
403
+ label="๐Ÿ“ฅ Download Animation",
404
+ data=gif_data,
405
+ file_name="lipsync_animation.gif",
406
+ mime="image/gif",
407
+ key=f"{key_prefix}_download",
408
+ use_container_width=True
409
+ )
410
+
411
+
412
+ def process_translation_pipeline(
413
+ text: str,
414
+ source_lang: str,
415
+ target_lang: str,
416
+ avatar_name: str
417
+ ) -> Tuple[str, Optional[str], Optional[str]]:
418
+ """Main processing pipeline: translate, synthesize speech, generate animation."""
419
+
420
+ # Step 1: Translate text
421
+ try:
422
+ translated_text = translate_text(text, source_lang, target_lang)
423
+ except Exception as e:
424
+ raise Exception(f"Translation failed: {str(e)}")
425
+
426
+ # Step 2: Synthesize speech
427
+ try:
428
+ audio_path = synthesize_speech(translated_text, target_lang, TEMP_DIR)
429
+ except Exception as e:
430
+ raise Exception(f"Speech synthesis failed: {str(e)}")
431
+
432
+ # Step 3: Generate lip-sync animation
433
+ gif_path = None
434
+ try:
435
+ gif_path = generate_lipsync_gif(
436
+ avatar_name=avatar_name,
437
+ audio_path=audio_path,
438
+ avatars_dir=AVATARS_DIR,
439
+ output_dir=TEMP_DIR,
440
+ fps=12
441
+ )
442
+ except Exception as e:
443
+ print(f"Animation generation warning: {str(e)}")
444
+ gif_path = None
445
+
446
+ return translated_text, audio_path, gif_path
447
+
448
+
449
+ # =============================================================================
450
+ # Sidebar
451
+ # =============================================================================
452
+
453
+ with st.sidebar:
454
+ st.markdown("## โš™๏ธ Settings")
455
+
456
+ # Avatar selection
457
+ st.markdown("### ๐ŸŽญ Avatar Selection")
458
+ avatars = list_avatars(AVATARS_DIR)
459
+
460
+ if avatars:
461
+ selected_avatar = st.selectbox(
462
+ "Choose your avatar",
463
+ options=avatars,
464
+ index=0,
465
+ help="Select an anime avatar for lip-sync animation"
466
+ )
467
+
468
+ preview = get_avatar_preview(selected_avatar, AVATARS_DIR)
469
+ if preview:
470
+ st.image(preview, caption=f"Preview: {selected_avatar}", use_container_width=True)
471
+ else:
472
+ st.warning("No avatars found. Creating sample avatar...")
473
+ ensure_sample_avatar(AVATARS_DIR)
474
+ selected_avatar = "sample"
475
+ st.rerun()
476
+
477
+ st.markdown("---")
478
+
479
+ # Language settings
480
+ st.markdown("### ๐ŸŒ Language Settings")
481
+
482
+ source_language = st.selectbox(
483
+ "Source Language",
484
+ options=["auto", "en", "hi"],
485
+ format_func=lambda x: {"auto": "๐Ÿ”„ Auto-detect", "en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x],
486
+ index=0
487
+ )
488
+
489
+ target_language = st.selectbox(
490
+ "Target Language",
491
+ options=["en", "hi"],
492
+ format_func=lambda x: {"en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x],
493
+ index=1
494
+ )
495
+
496
+ st.markdown("---")
497
+
498
+ # Animation settings
499
+ st.markdown("### ๐ŸŽฌ Animation Settings")
500
+
501
+ animation_size = st.slider(
502
+ "Animation Size",
503
+ min_value=200,
504
+ max_value=500,
505
+ value=350,
506
+ step=50,
507
+ help="Adjust the display size of the animation"
508
+ )
509
+
510
+ auto_play = st.checkbox("Auto-play animation", value=True)
511
+
512
+ st.markdown("---")
513
+
514
+ # System status
515
+ st.markdown("### ๐Ÿ”ง System Status")
516
+
517
+ ffmpeg_status = check_ffmpeg_detailed()
518
+
519
+ if ffmpeg_status["ffmpeg_works"]:
520
+ st.success("โœ… FFmpeg: Working")
521
+ else:
522
+ st.error("โŒ FFmpeg: Not working")
523
+
524
+ if ffmpeg_status["pydub_works"]:
525
+ st.success("โœ… Pydub: Working")
526
+ else:
527
+ st.warning("โš ๏ธ Pydub: Limited (fallback mode)")
528
+
529
+ if ffmpeg_status["error_message"]:
530
+ with st.expander("๐Ÿ” Error Details"):
531
+ st.code(ffmpeg_status["error_message"])
532
+ st.markdown("""
533
+ **To fix FFmpeg:**
534
+ ```bash
535
+ conda install -c conda-forge ffmpeg
536
+ ```
537
+ Or download from: https://www.gyan.dev/ffmpeg/builds/
538
+ """)
539
+
540
+ st.markdown("---")
541
+
542
+ # Info section
543
+ st.markdown("### โ„น๏ธ About")
544
+ st.markdown("""
545
+ Translate text between English and Hindi with lip-synced avatar animation.
546
+
547
+ **Features:**
548
+ - ๐ŸŽค Voice input
549
+ - ๐Ÿ”„ Auto detection
550
+ - ๐Ÿ—ฃ๏ธ Text-to-speech
551
+ - ๐ŸŽฌ Lip-sync animation
552
+ """)
553
+
554
+ if st.button("๐Ÿงน Clear Temp Files"):
555
+ cleanup_temp_files(older_than_sec=0)
556
+ st.success("Cleared!")
557
+
558
+
559
+ # =============================================================================
560
+ # Main Content
561
+ # =============================================================================
562
+
563
+ st.markdown('<h1 class="main-header">๐ŸŽŒ Anime Translator</h1>', unsafe_allow_html=True)
564
+ st.markdown(
565
+ '<p style="text-align: center; color: #888; font-size: 1.2rem;">'
566
+ 'Translate โ€ข Speak โ€ข Animate</p>',
567
+ unsafe_allow_html=True
568
+ )
569
+
570
+ # Tabs
571
+ tab1, tab2 = st.tabs(["๐Ÿ“ Text Input", "๐ŸŽค Voice Input"])
572
+
573
+ # =============================================================================
574
+ # Tab 1: Text Input
575
+ # =============================================================================
576
+
577
+ with tab1:
578
+ col1, col2 = st.columns([1, 1])
579
+
580
+ with col1:
581
+ st.markdown("### ๐Ÿ“ Enter Your Text")
582
+
583
+ text_input = st.text_area(
584
+ "Type or paste your text here",
585
+ height=150,
586
+ placeholder="Enter text in English or Hindi...\nเค‰เคฆเคพเคนเคฐเคฃ: เคจเคฎเคธเฅเคคเฅ‡, เค†เคช เค•เฅˆเคธเฅ‡ เคนเฅˆเค‚?\nExample: Hello, how are you?",
587
+ key="text_input"
588
+ )
589
+
590
+ if text_input:
591
+ detected = detect_language(text_input)
592
+ st.markdown(
593
+ f'<div class="info-box">'
594
+ f'๐Ÿ“Š Characters: {len(text_input)} | '
595
+ f'๐Ÿ” Detected: {"๐Ÿ‡ฎ๐Ÿ‡ณ Hindi" if detected == "hi" else "๐Ÿ‡ฌ๐Ÿ‡ง English"}'
596
+ f'</div>',
597
+ unsafe_allow_html=True
598
+ )
599
+
600
+ translate_btn = st.button(
601
+ "๐Ÿš€ Translate & Animate",
602
+ key="translate_text_btn",
603
+ use_container_width=True
604
+ )
605
+
606
+ with col2:
607
+ st.markdown("### ๐ŸŽฌ Result")
608
+
609
+ # Create a container with fixed height
610
+ result_container = st.container()
611
+
612
+ with result_container:
613
+ if translate_btn and text_input:
614
+ with st.spinner("๐Ÿ”„ Processing..."):
615
+ progress = st.progress(0)
616
+ status_text = st.empty()
617
+
618
+ try:
619
+ status_text.text("๐Ÿ“ Translating...")
620
+ progress.progress(33)
621
+
622
+ translated, audio_path, gif_path = process_translation_pipeline(
623
+ text_input,
624
+ source_language,
625
+ target_language,
626
+ selected_avatar
627
+ )
628
+
629
+ status_text.text("๐Ÿ—ฃ๏ธ Generating speech...")
630
+ progress.progress(66)
631
+
632
+ status_text.text("๐ŸŽฌ Creating animation...")
633
+ progress.progress(100)
634
+
635
+ progress.empty()
636
+ status_text.empty()
637
+
638
+ # Store results in session state
639
+ st.session_state['text_result'] = {
640
+ 'translated': translated,
641
+ 'audio_path': audio_path,
642
+ 'gif_path': gif_path
643
+ }
644
+ st.session_state['text_animation_playing'] = auto_play
645
+
646
+ except Exception as e:
647
+ progress.empty()
648
+ status_text.empty()
649
+ st.error(f"โŒ Error: {str(e)}")
650
+
651
+ elif translate_btn:
652
+ st.warning("โš ๏ธ Please enter some text to translate.")
653
+
654
+ # Display stored results
655
+ if 'text_result' in st.session_state:
656
+ result = st.session_state['text_result']
657
+
658
+ # Display translated text
659
+ st.markdown(
660
+ f'<div class="result-box">'
661
+ f'<h4>๐Ÿ“œ Translated Text:</h4>'
662
+ f'<p style="font-size: 1.2rem;">{result["translated"]}</p>'
663
+ f'</div>',
664
+ unsafe_allow_html=True
665
+ )
666
+
667
+ # Audio player
668
+ if result['audio_path'] and os.path.exists(result['audio_path']):
669
+ st.markdown("#### ๐Ÿ”Š Audio")
670
+ st.audio(result['audio_path'], format="audio/mp3")
671
+
672
+ # Animation display with controls
673
+ if result['gif_path'] and os.path.exists(result['gif_path']):
674
+ st.markdown("#### ๐ŸŽญ Lip-Sync Animation")
675
+ display_animation_with_controls(result['gif_path'], key_prefix="text")
676
+ else:
677
+ st.info("โ„น๏ธ Animation not available (FFmpeg may be missing)")
678
+
679
+
680
+ # =============================================================================
681
+ # Tab 2: Voice Input
682
+ # =============================================================================
683
+
684
+ with tab2:
685
+ col1, col2 = st.columns([1, 1])
686
+
687
+ with col1:
688
+ st.markdown("### ๐ŸŽค Voice Recording")
689
+
690
+ st.markdown("""
691
+ <div class="info-box">
692
+ <strong>Instructions:</strong><br>
693
+ 1. Upload an audio file (WAV, MP3, etc.)<br>
694
+ 2. Or use the audio recorder below<br>
695
+ 3. Click "Transcribe & Translate"
696
+ </div>
697
+ """, unsafe_allow_html=True)
698
+
699
+ uploaded_audio = st.file_uploader(
700
+ "Upload an audio file",
701
+ type=["wav", "mp3", "ogg", "flac", "m4a"],
702
+ help="Supported formats: WAV, MP3, OGG, FLAC, M4A"
703
+ )
704
+
705
+ recorded_audio = None
706
+ try:
707
+ from audio_recorder_streamlit import audio_recorder
708
+ st.markdown("**Or record directly:**")
709
+ recorded_audio = audio_recorder(
710
+ text="๐ŸŽ™๏ธ Click to record",
711
+ recording_color="#e94560",
712
+ neutral_color="#6c757d",
713
+ icon_name="microphone",
714
+ icon_size="2x"
715
+ )
716
+ except ImportError:
717
+ st.info("๐Ÿ’ก For recording: `pip install audio-recorder-streamlit`")
718
+
719
+ voice_lang = st.selectbox(
720
+ "Recording Language",
721
+ options=["en", "hi"],
722
+ format_func=lambda x: {"en": "๐Ÿ‡ฌ๐Ÿ‡ง English", "hi": "๐Ÿ‡ฎ๐Ÿ‡ณ Hindi"}[x]
723
+ )
724
+
725
+ voice_btn = st.button(
726
+ "๐ŸŽฏ Transcribe & Translate",
727
+ key="voice_btn",
728
+ use_container_width=True
729
+ )
730
+
731
+ with col2:
732
+ st.markdown("### ๐ŸŽฌ Result")
733
+
734
+ audio_to_process = None
735
+
736
+ if uploaded_audio is not None:
737
+ temp_audio_path = TEMP_DIR / f"uploaded_{int(time.time()*1000)}.wav"
738
+ with open(temp_audio_path, "wb") as f:
739
+ f.write(uploaded_audio.getbuffer())
740
+ audio_to_process = str(temp_audio_path)
741
+ st.audio(uploaded_audio)
742
+
743
+ elif recorded_audio is not None:
744
+ temp_audio_path = TEMP_DIR / f"recorded_{int(time.time()*1000)}.wav"
745
+ with open(temp_audio_path, "wb") as f:
746
+ f.write(recorded_audio)
747
+ audio_to_process = str(temp_audio_path)
748
+ st.audio(recorded_audio, format="audio/wav")
749
+
750
+ if voice_btn:
751
+ if audio_to_process:
752
+ with st.spinner("๐Ÿ”„ Processing voice..."):
753
+ try:
754
+ st.text("๐ŸŽค Transcribing...")
755
+ lang_code = get_language_code(voice_lang)
756
+ transcribed_text, success = transcribe_audio(audio_to_process, lang_code)
757
+
758
+ if success:
759
+ translated, audio_path, gif_path = process_translation_pipeline(
760
+ transcribed_text,
761
+ voice_lang,
762
+ target_language,
763
+ selected_avatar
764
+ )
765
+
766
+ # Store results in session state
767
+ st.session_state['voice_result'] = {
768
+ 'transcribed': transcribed_text,
769
+ 'translated': translated,
770
+ 'audio_path': audio_path,
771
+ 'gif_path': gif_path
772
+ }
773
+ st.session_state['voice_animation_playing'] = auto_play
774
+
775
+ else:
776
+ st.error(f"โŒ {transcribed_text}")
777
+ except Exception as e:
778
+ st.error(f"โŒ Error: {str(e)}")
779
+ else:
780
+ st.warning("โš ๏ธ Please upload or record audio first.")
781
+
782
+ # Display stored results
783
+ if 'voice_result' in st.session_state:
784
+ result = st.session_state['voice_result']
785
+
786
+ st.markdown(
787
+ f'<div class="success-box">'
788
+ f'<strong>๐Ÿ“ Transcribed:</strong> {result["transcribed"]}'
789
+ f'</div>',
790
+ unsafe_allow_html=True
791
+ )
792
+
793
+ st.markdown(
794
+ f'<div class="result-box">'
795
+ f'<h4>๐Ÿ“œ Translated:</h4>'
796
+ f'<p style="font-size: 1.2rem;">{result["translated"]}</p>'
797
+ f'</div>',
798
+ unsafe_allow_html=True
799
+ )
800
+
801
+ if result['audio_path'] and os.path.exists(result['audio_path']):
802
+ st.markdown("#### ๐Ÿ”Š Audio")
803
+ st.audio(result['audio_path'], format="audio/mp3")
804
+
805
+ if result['gif_path'] and os.path.exists(result['gif_path']):
806
+ st.markdown("#### ๐ŸŽญ Animation")
807
+ display_animation_with_controls(result['gif_path'], key_prefix="voice")
808
+
809
+
810
+ # =============================================================================
811
+ # Footer
812
+ # =============================================================================
813
+
814
+ st.markdown("---")
815
+ st.markdown(
816
+ """
817
+ <div style="text-align: center; color: #666; padding: 1rem;">
818
+ <p>Made By Praveen</p>
819
+ </div>
820
+ """,
821
+ unsafe_allow_html=True
822
  )