| |
| |
| |
| |
| |
|
|
|
|
| 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() |
|
|