From d548bd6fdec3d3ccceff373ee26e155975f02193 Mon Sep 17 00:00:00 2001 From: default Date: Wed, 7 Jan 2026 04:34:24 +0000 Subject: [PATCH] YT Audio Encoding --- README.md | 1 + server/app.py | 50 ++++- server/requirements.txt | 1 + server/youtube_utils.py | 52 +++++ src/lib/components/YouTubeEncoderTool.svelte | 190 +++++++++++++++++++ src/routes/+page.svelte | 10 + 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 server/youtube_utils.py create mode 100644 src/lib/components/YouTubeEncoderTool.svelte diff --git a/README.md b/README.md index a7cbb0b..796fea6 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ AudioImage is a powerful web-based tool for Audio Spectrogram Art and Digital St * **Process Visualizer**: A real-time, interactive visualization page that lets you watch and hear the transformation process. * **Live Waveform**: See the audio signal rendered as a glowing, real-time oscilloscope. * **Step-by-Step Reveal**: Watch the spectrogram and steganographic image being constructed layer by layer. +* **YouTube Audio Encoder**: Directly download audio from YouTube videos (with length validation) and embed it into images seamlessly. * **Audio Art Generation**: Convert MP3/AAC audio files into high-resolution visual spectrograms using Python's `librosa` and `matplotlib`. * **GPU Acceleration**: Automatically uses `torchaudio` and CUDA if available for lightning-fast processing. * **Steganography Hider**: Hide secret audio or image files inside a "host" PNG image effectively using LSB (Least Significant Bit) encoding. diff --git a/server/app.py b/server/app.py index a1b5207..579d76c 100644 --- a/server/app.py +++ b/server/app.py @@ -102,16 +102,21 @@ def generate_art(): @app.route('/api/hide', methods=['POST']) def hide_data(): if 'data' not in request.files or 'host' not in request.files: - return jsonify({"error": "Requires 'data' and 'host' files"}), 400 + return jsonify({"error": "Missing files"}), 400 + data_file = request.files['data'] + host_file = request.files['host'] + data_path = None host_path = None + try: - data_path = save_upload(request.files['data']) - host_path = save_upload(request.files['host']) + data_path = save_upload(data_file) + host_path = save_upload(host_file) + + output_path = processor.encode_stego(data_path, host_path) + return send_file(output_path, mimetype='image/png') - stego_path = processor.encode_stego(data_path, host_path) - return send_file(stego_path, mimetype='image/png') except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: @@ -124,6 +129,41 @@ def hide_data(): try: os.remove(host_path) except: pass +import youtube_utils + +@app.route('/api/hide-yt', methods=['POST']) +def hide_yt_data(): + if 'url' not in request.form or 'host' not in request.files: + return jsonify({"error": "Missing URL or Host Image"}), 400 + + youtube_url = request.form['url'] + host_file = request.files['host'] + + audio_path = None + host_path = None + + try: + # Download Audio + audio_path = youtube_utils.download_audio(youtube_url, app.config['UPLOAD_FOLDER']) + + # Save Host + host_path = save_upload(host_file) + + # Encode + output_path = processor.encode_stego(audio_path, host_path) + return send_file(output_path, mimetype='image/png') + + except Exception as e: + return jsonify({"error": str(e)}), 500 + finally: + # Cleanup + if audio_path and os.path.exists(audio_path): + try: os.remove(audio_path) + except: pass + if host_path and os.path.exists(host_path): + try: os.remove(host_path) + except: pass + @app.route('/api/decode', methods=['POST']) def decode(): if 'image' not in request.files: diff --git a/server/requirements.txt b/server/requirements.txt index f8656e3..907a3c6 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -7,3 +7,4 @@ librosa matplotlib torch torchaudio +yt-dlp diff --git a/server/youtube_utils.py b/server/youtube_utils.py new file mode 100644 index 0000000..2998b03 --- /dev/null +++ b/server/youtube_utils.py @@ -0,0 +1,52 @@ +import yt_dlp +import os +import time + +def download_audio(url, output_folder, max_length_seconds=600): + """ + Downloads audio from a YouTube URL. + Returns the path to the downloaded file or raises an Exception. + Enforces max_length_seconds (default 10 mins). + """ + + timestamp = int(time.time()) + output_template = os.path.join(output_folder, f'yt_{timestamp}_%(id)s.%(ext)s') + + ydl_opts = { + 'format': 'bestaudio/best', + 'outtmpl': output_template, + 'postprocessors': [{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': 'mp3', + 'preferredquality': '192', + }], + 'quiet': True, + 'no_warnings': True, + 'noplaylist': True, + 'match_filter': yt_dlp.utils.match_filter_func("duration <= " + str(max_length_seconds)) + } + + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + # The file path might differ slightly because of the postprocessor (mp3 conversion) + # yt-dlp usually returns the final filename in 'requested_downloads' or similar, + # but constructing it from info is safer if we know the template. + # However, extract_info returns the info dict. + + # Since we force mp3, the file will end in .mp3 + # We used %(id)s in template, so we can reconstruct or find it. + + # Let's find the file in the folder that matches the timestamp prefix + # This is safer than guessing what yt-dlp named it exactly + + valid_files = [f for f in os.listdir(output_folder) if f.startswith(f'yt_{timestamp}_') and f.endswith('.mp3')] + if not valid_files: + raise Exception("Download failed: Audio file not found after processing.") + + return os.path.join(output_folder, valid_files[0]) + + except yt_dlp.utils.DownloadError as e: + if "video is too long" in str(e).lower() or "duration" in str(e).lower(): + raise Exception(f"Video is too long. Maximum allowed duration is {max_length_seconds} seconds.") + raise Exception(f"Failed to download video: {str(e)}") diff --git a/src/lib/components/YouTubeEncoderTool.svelte b/src/lib/components/YouTubeEncoderTool.svelte new file mode 100644 index 0000000..d016d31 --- /dev/null +++ b/src/lib/components/YouTubeEncoderTool.svelte @@ -0,0 +1,190 @@ + + +
+

+ + YouTube Encoder +

+
+

+ Download audio from a YouTube video and hide it inside an image. +

+ +
+ +
+ +
+
+ +
+ +
+ {#if youtubeUrl && getYoutubeVideoId(youtubeUrl)} +
+ Valid YouTube URL detected +
+ {/if} +
+ + +
+
+ + {#if stegoHostFile && hostImageMP > 0} +
+ {hostImageMP.toFixed(2)} MP +
+ {/if} +
+ +
e.preventDefault()} + ondrop={(e) => { + e.preventDefault(); + if (e.dataTransfer?.files && e.dataTransfer.files[0]) { + const file = e.dataTransfer.files[0]; + stegoHostFile = file; + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { + hostImageMP = (img.width * img.height) / 1000000; + URL.revokeObjectURL(url); + }; + img.src = url; + } + }} + onclick={() => !loading && document.getElementById('yt-host-input')?.click()} + onkeydown={(e) => + (e.key === 'Enter' || e.key === ' ') && + document.getElementById('yt-host-input')?.click()} + > + { + const target = e.target as HTMLInputElement; + if (target && target.files && target.files[0]) { + const file = target.files[0]; + stegoHostFile = file; + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { + hostImageMP = (img.width * img.height) / 1000000; + URL.revokeObjectURL(url); + }; + img.src = url; + } + }} + /> + + {#if stegoHostFile} +
+ +
{stegoHostFile.name}
+
+ {(stegoHostFile.size / 1024 / 1024).toFixed(2)} MB +
+
+ {:else} +
+ +
Drop PNG image here or click to browse
+
+ {/if} +
+
+ + +
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7d7aa83..59c235e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,6 +5,7 @@ import ResultPreview from '$lib/components/ResultPreview.svelte'; import SpectrogramTool from '$lib/components/SpectrogramTool.svelte'; import EncoderTool from '$lib/components/EncoderTool.svelte'; + import YouTubeEncoderTool from '$lib/components/YouTubeEncoderTool.svelte'; import DecoderTool from '$lib/components/DecoderTool.svelte'; let loading = false; @@ -107,6 +108,15 @@ on:error={handleError} on:complete={handleEncoderComplete} /> + + +