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
⏰ روی کلمات کلیک کن تا به زمان موردنظر بری • کلمات با صدا روشن میشوند
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()