Spaces:
Running
Running
| import os | |
| import torch | |
| import gradio as gr | |
| from transformers import pipeline | |
| import transformers | |
| import torch | |
| import wave | |
| import contextlib | |
| import tempfile | |
| from pydub import AudioSegment | |
| MODEL_REPO_ID = os.environ["MODEL_REPO_ID"] | |
| HF_TOKEN = os.environ["HF_TOKEN"] | |
| device = 0 if torch.cuda.is_available() else -1 | |
| SEGMENT_LIMIT = 300 # 300s = 5 minutes | |
| pipe = pipeline( | |
| task="automatic-speech-recognition", | |
| model=MODEL_REPO_ID, | |
| chunk_length_s=30, | |
| device=device, | |
| token=HF_TOKEN | |
| ) | |
| def transcribe(file): | |
| if file is None: | |
| return "Please select an audio file." | |
| warning = "" | |
| try: | |
| all_results = [] | |
| result = pipe( | |
| file, | |
| batch_size=8, | |
| generate_kwargs={ | |
| "task": "transcribe", | |
| "num_beams": 3 | |
| }, | |
| ) | |
| if isinstance(result, dict) and 'chunks' in result: | |
| all_results.extend(result['chunks']) | |
| if all_results: | |
| timestamped = "\n".join([ | |
| f"[{chunk['timestamp'][0]:.2f}:{chunk['timestamp'][1]:.2f}] {chunk['text'].strip()}" | |
| for chunk in all_results if chunk['text'].strip() | |
| ]) | |
| return timestamped + warning | |
| else: | |
| transcription = result.get('text', 'No transcription available') if isinstance(result, dict) else str(result) | |
| return transcription + warning | |
| except Exception as e: | |
| return f"Error during transcription: {str(e)}" | |
| def transcribe_word_timestamps(file): | |
| if file is None: | |
| return "Please select an audio file.", "", "" | |
| if hasattr(file, 'name'): | |
| file_path = file.name | |
| else: | |
| file_path = file | |
| warning = "" | |
| try: | |
| all_chunks = [] | |
| result = pipe( | |
| file_path, | |
| generate_kwargs={ | |
| "task": "transcribe", | |
| "num_beams": 3, | |
| "condition_on_prev_tokens": False, | |
| }, | |
| return_timestamps="word" | |
| ) | |
| if isinstance(result, dict) and 'chunks' in result: | |
| all_chunks.extend(result['chunks']) | |
| if all_chunks: | |
| sample_text = ' '.join([chunk['text'].strip() for chunk in all_chunks[:3]]) | |
| is_rtl = any('\u0600' <= char <= '\u06FF' for char in sample_text) | |
| import base64 | |
| with open(file_path, 'rb') as audio_file: | |
| audio_data = audio_file.read() | |
| audio_base64 = base64.b64encode(audio_data).decode() | |
| # Determine MIME type based on file extension | |
| file_ext = file_path.lower().split('.')[-1] | |
| if file_ext in ['mp3', 'mpeg']: | |
| audio_mime = "audio/mpeg" | |
| elif file_ext in ['wav']: | |
| audio_mime = "audio/wav" | |
| elif file_ext in ['ogg']: | |
| audio_mime = "audio/ogg" | |
| elif file_ext in ['m4a', 'aac']: | |
| audio_mime = "audio/aac" | |
| else: | |
| audio_mime = "audio/mpeg" # Default fallback | |
| audio_url = f"data:{audio_mime};base64,{audio_base64}" | |
| html_content = f''' | |
| <div style="max-width: 800px; min-width: 800px; margin: 0 auto; padding: 20px; border: 1px solid #444; border-radius: 10px; background: #2d2d2d; color: #fff;"> | |
| <!-- Custom Audio Player --> | |
| <div style="background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);"> | |
| <audio id="custom-audio-player" preload="metadata" style="width: 100%; margin-bottom: 10px;"> | |
| <source src="{audio_url}" type="{audio_mime}"> | |
| Your browser does not support the audio element. | |
| </audio> | |
| <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"> | |
| <button id="play-pause-btn" style="background: #0d6efd; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; transition: all 0.2s; min-width: 55px;"> | |
| ▶️ | |
| </button> | |
| <span id="time-display" style="font-family: monospace; font-size: 14px; color: #ccc;"> | |
| 00:00 / 00:00 | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Transcription with highlighting --> | |
| <div style="background: #1a1a1a; padding: 15px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);"> | |
| <h3 style="margin-top: 0; color: #fff;">Transcription:</h3> | |
| <div id="transcription-text" style="line-height: 1.8; padding: 15px; border: 1px solid #444; border-radius: 5px; direction: {"rtl" if is_rtl else "ltr"}; text-align: {"right" if is_rtl else "left"}; background: transparent; color: #fff;"> | |
| ''' | |
| for i, chunk in enumerate(all_chunks): | |
| word = chunk['text'].strip() | |
| start_time = chunk['timestamp'][0] | |
| end_time = chunk['timestamp'][1] if chunk['timestamp'][1] is not None else start_time + 0.5 | |
| # Active highlight color fallback placeholder handled via plain inline styles safely | |
| html_content += f'<span class="word-span" data-word-index="{i}" data-start="{start_time}" data-end="{end_time}" style="cursor: pointer; padding: 4px 6px; margin: 2px; border-radius: 4px; display: inline-block; transition: all 0.2s; background: transparent; border: 1px solid transparent; color: #fff;">{word}</span> ' | |
| html_content += ''' | |
| </div> | |
| <div style="margin-top: 10px; font-size: 12px; color: #888;"> | |
| 🎯 Click on any word to jump to that timestamp • Words will highlight as audio plays | |
| </div> | |
| <div style="margin-top: 5px; font-size: 12px; color: #888;"> | |
| ⏰ روی کلمات کلیک کن تا به زمان موردنظر بری • کلمات با صدا روشن میشوند | |
| </div> | |
| </div> | |
| </div> | |
| ''' | |
| import json | |
| words_json = json.dumps(all_chunks) | |
| return html_content, words_json, warning | |
| else: | |
| transcription = result.get('text', 'No transcription available') if isinstance(result, dict) else str(result) | |
| return f'<div style="min-height: 200px; color: #fff; background: #2d2d2d; padding: 20px; border-radius: 10px; border: 1px solid #444;">{transcription}</div>', "[]", warning | |
| except Exception as e: | |
| return f'<div style="min-height: 200px; color: #fff; background: #2d2d2d; padding: 20px; border-radius: 10px; border: 1px solid #444;">Error during word timestamp transcription: {str(e)}</div>', "[]", "" | |
| basic_interface = gr.Interface( | |
| fn=transcribe, | |
| inputs=[ | |
| gr.Audio(sources=["upload", "microphone"], type="filepath", label="Upload or Record Audio") | |
| ], | |
| outputs=gr.Textbox(label="Transcription"), | |
| title="C1Tech Whisper Persian/فارسی", | |
| description="Upload an audio file or record directly. Outputs transcription." | |
| ) | |
| with gr.Blocks( | |
| title="C1Tech Whisper Persian Transcription", | |
| theme=gr.themes.Base(), | |
| css="style.css" | |
| ) as demo: | |
| gr.Markdown(""" | |
| # 🎙️ C1Tech Whisper Persian - Audio Transcription with Timestamps | |
| This application provides Persian speech-to-text transcription with precise timestamps. | |
| Choose the transcription type below. Both support file upload and microphone recording. | |
| </br>این برنامه گفتار فارسی را به متن تبدیل میکند و زمان دقیق هر بخش را هم مشخص میکند | |
| میتوانید نوع تبدیل متن را از گزینههای زیر انتخاب کنید. هر دو گزینه از بارگذاری فایل و ضبط با میکروفون پشتیبانی میکنند | |
| """) | |
| with gr.Tab("🔤 Word-Level Timestamps"): | |
| with gr.Column(): | |
| audio_input = gr.File( | |
| file_types=["audio"], | |
| label="Upload Audio File", | |
| file_count="single" | |
| ) | |
| transcribe_btn = gr.Button("Transcribe", variant="primary") | |
| word_level_output = gr.HTML( | |
| label="Audio Player with Word-Level Highlighting", | |
| elem_id="transcription_display", | |
| value='<div style="min-height: 40px; padding: 20px; text-align: center; color: #888; background: #2d2d2d; border: 1px solid #444; border-radius: 10px;">Upload an audio file and click "Transcribe" to see the interactive transcription with word-level timestamps.</div>' | |
| ) | |
| words_data = gr.Textbox(visible=False, elem_id="words_data_hidden") | |
| warning_output = gr.Textbox(label="Warning", visible=False) | |
| def transcribe_and_setup(file): | |
| html, json_data, warning = transcribe_word_timestamps(file) | |
| return html, json_data, warning | |
| transcribe_btn.click( | |
| fn=transcribe_and_setup, | |
| inputs=[audio_input], | |
| outputs=[word_level_output, words_data, warning_output], | |
| js=f""" | |
| async function(file) {{ | |
| return file; | |
| }} | |
| """ | |
| ).then( | |
| fn=None, | |
| inputs=[word_level_output, words_data], | |
| outputs=None, | |
| js=""" | |
| function(html_content, words_json) { | |
| if (!words_json || words_json === 'null' || words_json.trim() === '') { | |
| return; | |
| } | |
| try { | |
| var chunks = JSON.parse(words_json); | |
| if (!chunks || !chunks.length) return; | |
| } catch(e) { | |
| return; | |
| } | |
| setTimeout(function() { | |
| var audio = document.getElementById('custom-audio-player'); | |
| var playPauseBtn = document.getElementById('play-pause-btn'); | |
| var timeDisplay = document.getElementById('time-display'); | |
| var wordSpans = document.querySelectorAll('.word-span'); | |
| if (!audio || !playPauseBtn) return; | |
| var isPlaying = false; | |
| var duration = 0; | |
| function formatTime(seconds) { | |
| if (isNaN(seconds) || !isFinite(seconds)) return '00:00'; | |
| var mins = Math.floor(seconds / 60); | |
| var secs = Math.floor(seconds % 60); | |
| return mins.toString().padStart(2, '0') + ':' + secs.toString().padStart(2, '0'); | |
| } | |
| function updateDisplay() { | |
| var currentTime = audio.currentTime || 0; | |
| duration = audio.duration || 0; | |
| if (timeDisplay) { | |
| timeDisplay.textContent = formatTime(currentTime) + ' / ' + formatTime(duration); | |
| } | |
| } | |
| // Track actual play/pause states reliably directly from native events | |
| audio.addEventListener('play', function() { | |
| isPlaying = true; | |
| playPauseBtn.textContent = '⏸️'; | |
| }); | |
| audio.addEventListener('pause', function() { | |
| isPlaying = false; | |
| playPauseBtn.textContent = '▶️'; | |
| }); | |
| audio.addEventListener('ended', function() { | |
| isPlaying = false; | |
| playPauseBtn.textContent = '▶️'; | |
| }); | |
| playPauseBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| if (isPlaying) { | |
| audio.pause(); | |
| } else { | |
| audio.play().catch(function(err) { console.error(err); }); | |
| } | |
| }); | |
| audio.addEventListener('loadedmetadata', function() { | |
| duration = audio.duration; | |
| updateDisplay(); | |
| }); | |
| audio.addEventListener('timeupdate', function() { | |
| var currentTime = audio.currentTime; | |
| updateDisplay(); | |
| // Clear active word states back to baseline | |
| for (var i = 0; i < wordSpans.length; i++) { | |
| var span = wordSpans[i]; | |
| // Only touch non-hovered tokens to ensure style locks remain fluid | |
| if (span.getAttribute('data-is-active') === 'true') { | |
| span.style.backgroundColor = 'transparent'; | |
| span.style.color = '#fff'; | |
| span.style.fontWeight = 'normal'; | |
| span.style.transform = 'scale(1)'; | |
| span.style.borderColor = 'transparent'; | |
| span.removeAttribute('data-is-active'); | |
| } | |
| } | |
| // Highlight current word match | |
| for (var i = 0; i < wordSpans.length; i++) { | |
| var span = wordSpans[i]; | |
| var start = parseFloat(span.getAttribute('data-start')); | |
| var end = parseFloat(span.getAttribute('data-end')); | |
| if (currentTime >= start && currentTime <= end) { | |
| span.style.backgroundColor = '#0d6efd'; | |
| span.style.color = '#fff'; | |
| span.style.fontWeight = 'bold'; | |
| span.style.transform = 'scale(1.05)'; | |
| span.style.borderColor = '#0d6efd'; | |
| span.setAttribute('data-is-active', 'true'); | |
| break; | |
| } | |
| } | |
| }); | |
| // Hook interaction handlers safely | |
| for (var i = 0; i < wordSpans.length; i++) { | |
| (function(index, span) { | |
| span.addEventListener('click', function() { | |
| var start = parseFloat(this.getAttribute('data-start')); | |
| audio.currentTime = start; | |
| audio.play().catch(function(err){}); | |
| }); | |
| span.addEventListener('mouseenter', function() { | |
| // Only apply hover treatment if it isn't the currently spoken active word | |
| if (this.getAttribute('data-is-active') !== 'true') { | |
| this.style.backgroundColor = '#555'; | |
| this.style.borderColor = '#666'; | |
| } | |
| }); | |
| span.addEventListener('mouseleave', function() { | |
| // Gracefully fall back to seamless transparency if it's not active | |
| if (this.getAttribute('data-is-active') !== 'true') { | |
| this.style.backgroundColor = 'transparent'; | |
| this.style.borderColor = 'transparent'; | |
| } | |
| }); | |
| })(i, wordSpans[i]); | |
| } | |
| setTimeout(updateDisplay, 100); | |
| }, 300); | |
| } | |
| """ | |
| ) | |
| with gr.Tab("📝 Basic Transcription"): | |
| basic_interface.render() | |
| if __name__ == "__main__": | |
| demo.queue().launch() |