|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import os |
|
|
import time |
|
|
import wave |
|
|
import modal |
|
|
import contextlib |
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
def process_audio(original_audio_path, dubbed_audio_path, email, company_name, tolerance): |
|
|
""" |
|
|
This function processes the audio files, handling the logic for duration check, |
|
|
file upload to presigned URLs, and triggering the processing. |
|
|
""" |
|
|
|
|
|
modal_token_id = os.environ['MODAL_TOKEN_ID'] |
|
|
modal_token_secret = os.environ['MODAL_TOKEN_SECRET'] |
|
|
modal_environment = os.environ['MODAL_ENVIRONMENT'] |
|
|
modal_volume = os.environ['MODAL_VOLUME'] |
|
|
processing_id = str(int(time.time())) |
|
|
try: |
|
|
with contextlib.closing(wave.open(original_audio_path, 'r')) as f: |
|
|
frames = f.getnframes() |
|
|
rate = f.getframerate() |
|
|
original_duration = frames / float(rate) |
|
|
|
|
|
with contextlib.closing(wave.open(dubbed_audio_path, 'r')) as f: |
|
|
frames = f.getnframes() |
|
|
rate = f.getframerate() |
|
|
dubbed_duration = frames / float(rate) |
|
|
|
|
|
if original_duration > 1800 or dubbed_duration > 1800: |
|
|
return "Error: Audio duration exceeds 30 minutes." |
|
|
|
|
|
except Exception as e: |
|
|
return f"Error reading audio files: {e}" |
|
|
|
|
|
try: |
|
|
bsodtv_storage = modal.Volume.from_name(modal_volume) |
|
|
with bsodtv_storage.batch_upload() as batch: |
|
|
batch.put_file(original_audio_path, "/{}/original_audio.wav".format(processing_id)) |
|
|
batch.put_file(dubbed_audio_path, "/{}/original_audio.wav".format(processing_id)) |
|
|
bsodtv_storage.commit() |
|
|
except: |
|
|
return "Error uploading audio files to Cloud Storage." |
|
|
|
|
|
try: |
|
|
waveform_matching_function = modal.Function.from_name("Waveform-Matching", "reception_handler") |
|
|
waveform_matching_function.spawn( |
|
|
processing_id=processing_id, |
|
|
original_file="/{}/original_audio.wav".format(processing_id), |
|
|
dubbed_file="/{}/original_audio.wav".format(processing_id), |
|
|
email=email, |
|
|
company_name=company_name, |
|
|
tolerance_percentage=tolerance |
|
|
) |
|
|
except: |
|
|
return "Error calling Outpost to trigger processing." |
|
|
return "Processing started. Results will be emailed to you shortly." |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
css = """ |
|
|
:root { |
|
|
--main-bg-color: #111827; |
|
|
--primary-color: #3B82F6; |
|
|
--secondary-color: #60A5FA; |
|
|
--text-color: #F9FAFB; |
|
|
--text-secondary: #9CA3AF; |
|
|
--card-bg: #1F2937; |
|
|
--border-color: #374151; |
|
|
--accent-blue: #3B82F6; |
|
|
--accent-yellow: #FBBF24; |
|
|
--accent-red: #EF4444; |
|
|
--accent-green: #22C55E; |
|
|
--border-radius: 8px; |
|
|
--golden-ratio: 1.618; |
|
|
--font-header: 'Barlow', sans-serif; |
|
|
--font-body: 'Work Sans', sans-serif; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: var(--font-body); |
|
|
background-color: var(--main-bg-color); |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
padding: calc(20px * var(--golden-ratio)); |
|
|
background-color: var(--main-bg-color); |
|
|
border-radius: calc(var(--border-radius) * var(--golden-ratio)); |
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
.logo-container { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
margin-bottom: calc(20px * var(--golden-ratio)); |
|
|
padding: 15px; |
|
|
background-color: var(--card-bg); |
|
|
border-radius: var(--border-radius); |
|
|
border: 1px solid var(--border-color); |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
.logo { |
|
|
max-width: 200px; |
|
|
max-height: 100px; |
|
|
transition: transform 0.3s ease; |
|
|
} |
|
|
|
|
|
.logo:hover { |
|
|
transform: scale(1.05); |
|
|
} |
|
|
|
|
|
.header { |
|
|
text-align: center; |
|
|
margin-bottom: calc(30px * var(--golden-ratio)); |
|
|
padding: calc(15px * var(--golden-ratio)); |
|
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); |
|
|
color: white; |
|
|
border-radius: var(--border-radius); |
|
|
box-shadow: 0 4px 10px rgba(59, 130, 246, 0.3); |
|
|
} |
|
|
|
|
|
.header h1 { |
|
|
color: white; |
|
|
font-family: var(--font-header); |
|
|
font-size: calc(1.5rem * var(--golden-ratio)); |
|
|
margin-bottom: calc(0.5rem * var(--golden-ratio)); |
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.header p { |
|
|
color: rgba(255, 255, 255, 0.9); |
|
|
font-size: 1rem; |
|
|
max-width: calc(600px * var(--golden-ratio)); |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
.input-section, .output-section { |
|
|
background-color: var(--card-bg); |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: var(--border-radius); |
|
|
padding: calc(20px * var(--golden-ratio)); |
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); |
|
|
margin-bottom: 20px; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.input-section:hover, .output-section:hover { |
|
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3); |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.input-section { |
|
|
flex: var(--golden-ratio); |
|
|
} |
|
|
|
|
|
.output-section { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.footer { |
|
|
text-align: center; |
|
|
margin-top: calc(30px * var(--golden-ratio)); |
|
|
padding: 15px; |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.9rem; |
|
|
border-top: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
/* Improve form elements */ |
|
|
.gradio-slider input[type=range] { |
|
|
accent-color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.gradio-textbox input, .gradio-textbox textarea { |
|
|
background-color: var(--main-bg-color) !important; |
|
|
border: 1px solid var(--border-color) !important; |
|
|
border-radius: var(--border-radius) !important; |
|
|
padding: 10px !important; |
|
|
color: var(--text-color) !important; |
|
|
transition: all 0.3s ease !important; |
|
|
} |
|
|
|
|
|
.gradio-textbox input:focus, .gradio-textbox textarea:focus { |
|
|
border-color: var(--primary-color) !important; |
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important; |
|
|
} |
|
|
|
|
|
.gradio-button { |
|
|
background-color: var(--primary-color) !important; |
|
|
color: white !important; |
|
|
border-radius: var(--border-radius) !important; |
|
|
padding: calc(10px * var(--golden-ratio)) calc(20px * var(--golden-ratio)) !important; |
|
|
font-weight: 600 !important; |
|
|
font-family: var(--font-header) !important; |
|
|
transition: all 0.3s ease !important; |
|
|
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3) !important; |
|
|
border: none !important; |
|
|
} |
|
|
|
|
|
.gradio-button:hover { |
|
|
background-color: var(--secondary-color) !important; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 6px 12px rgba(59, 130, 246, 0.4) !important; |
|
|
} |
|
|
|
|
|
/* Golden ratio spacing for elements */ |
|
|
.gradio-row { |
|
|
margin-bottom: calc(16px * var(--golden-ratio)) !important; |
|
|
} |
|
|
|
|
|
/* Additional dark theme adjustments */ |
|
|
.gradio-container { |
|
|
background-color: var(--main-bg-color) !important; |
|
|
} |
|
|
|
|
|
.gradio-form { |
|
|
background-color: var(--card-bg) !important; |
|
|
border: 1px solid var(--border-color) !important; |
|
|
} |
|
|
|
|
|
/* Labels and text styling */ |
|
|
label { |
|
|
color: var(--text-color) !important; |
|
|
font-family: var(--font-body) !important; |
|
|
} |
|
|
|
|
|
/* Responsive adjustments */ |
|
|
@media (max-width: 768px) { |
|
|
.container { |
|
|
padding: 15px; |
|
|
} |
|
|
|
|
|
.input-section, .output-section { |
|
|
padding: 15px; |
|
|
} |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue="purple")) as demo: |
|
|
with gr.Column(elem_classes="container"): |
|
|
|
|
|
with gr.Row(elem_classes="logo-container"): |
|
|
logo = gr.Image("logo.png", elem_id="logo") |
|
|
|
|
|
|
|
|
with gr.Column(elem_classes="header"): |
|
|
gr.HTML(""" |
|
|
<h1 style="margin-top: 0;">BSOD.tv - Dub QC Demo</h1> |
|
|
<p style="font-size: 1.1rem; line-height: 1.618;"> |
|
|
Professional audio synchronization verification for media localization. |
|
|
<br>Upload original and dubbed .wav files (under 30 minutes) to start the QC process. |
|
|
</p> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
|
|
|
with gr.Column(scale=1.618, elem_classes="input-section"): |
|
|
original_audio = gr.Audio(type="filepath", label="Original .wav file") |
|
|
dubbed_audio = gr.Audio(type="filepath", label="Dubbed .wav file") |
|
|
email = gr.Textbox(label="Email") |
|
|
company_name = gr.Textbox(label="Company Name") |
|
|
tolerance = gr.Slider(0, 100, value=5, label="Tolerance Percentage", |
|
|
info="Set the tolerance for audio comparison.") |
|
|
submit_btn = gr.Button("Process Audio", variant="primary") |
|
|
|
|
|
|
|
|
with gr.Column(scale=1, elem_classes="output-section"): |
|
|
output = gr.Text(label="Processing Status") |
|
|
gr.Markdown("### Results") |
|
|
gr.Markdown("Once processing is complete, results will be emailed to the address provided.") |
|
|
|
|
|
|
|
|
with gr.Row(elem_classes="footer"): |
|
|
gr.Markdown("© BSOD.tv - Professional Dub Quality Control") |
|
|
|
|
|
|
|
|
submit_btn.click( |
|
|
fn=process_audio, |
|
|
inputs=[original_audio, dubbed_audio, email, company_name, tolerance], |
|
|
outputs=output |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
|
|
|
demo.launch() |
|
|
|