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'''
00:00 / 00:00

Transcription:

''' 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'{word} ' html_content += '''
🎯 Click on any word to jump to that timestamp • Words will highlight as audio plays
⏰ روی کلمات کلیک کن تا به زمان موردنظر بری • کلمات با صدا روشن می‌شوند
''' 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'
{transcription}
', "[]", warning except Exception as e: return f'
Error during word timestamp transcription: {str(e)}
', "[]", "" 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.
این برنامه گفتار فارسی را به متن تبدیل می‌کند و زمان دقیق هر بخش را هم مشخص می‌کند می‌توانید نوع تبدیل متن را از گزینه‌های زیر انتخاب کنید. هر دو گزینه از بارگذاری فایل و ضبط با میکروفون پشتیبانی می‌کنند """) 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='
Upload an audio file and click "Transcribe" to see the interactive transcription with word-level timestamps.
' ) 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()