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 @@
+
+
+
+ Download audio from a YouTube video and hide it inside an image.
+
+
+