Code Update
This commit is contained in:
@@ -6,11 +6,9 @@ from flask_cors import CORS
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from processor import AudioImageProcessor
|
from processor import AudioImageProcessor
|
||||||
|
|
||||||
# Serve the build folder from the parent directory
|
|
||||||
app = Flask(__name__, static_folder='../build', static_url_path='')
|
app = Flask(__name__, static_folder='../build', static_url_path='')
|
||||||
CORS(app) # Allow Svelte to communicate
|
CORS(app)
|
||||||
|
|
||||||
# Configuration
|
|
||||||
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads')
|
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads')
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
processor = AudioImageProcessor(UPLOAD_FOLDER)
|
processor = AudioImageProcessor(UPLOAD_FOLDER)
|
||||||
@@ -21,29 +19,24 @@ def save_upload(file_obj):
|
|||||||
file_obj.save(path)
|
file_obj.save(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# --- Frontend Routes ---
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def not_found(e):
|
def not_found(e):
|
||||||
# If the path starts with /api, return actual 404
|
|
||||||
if request.path.startswith('/api/'):
|
if request.path.startswith('/api/'):
|
||||||
return jsonify({"error": "Not found"}), 404
|
return jsonify({"error": "Not found"}), 404
|
||||||
# Otherwise return index.html for SPA routing
|
|
||||||
return send_from_directory(app.static_folder, 'index.html')
|
return send_from_directory(app.static_folder, 'index.html')
|
||||||
|
|
||||||
@app.route('/health', methods=['GET'])
|
@app.route('/health', methods=['GET'])
|
||||||
def health():
|
def health():
|
||||||
return jsonify({"status": "ok", "max_mb": 40})
|
return jsonify({"status": "ok", "max_mb": 40})
|
||||||
|
|
||||||
# --- Background Cleanup ---
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
def cleanup_task():
|
def cleanup_task():
|
||||||
"""Background thread to clean up old files."""
|
expiration_seconds = 600
|
||||||
expiration_seconds = 600 # 10 minutes
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -51,7 +44,6 @@ def cleanup_task():
|
|||||||
for filename in os.listdir(UPLOAD_FOLDER):
|
for filename in os.listdir(UPLOAD_FOLDER):
|
||||||
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
filepath = os.path.join(UPLOAD_FOLDER, filename)
|
||||||
if os.path.isfile(filepath):
|
if os.path.isfile(filepath):
|
||||||
# check creation time
|
|
||||||
if now - os.path.getctime(filepath) > expiration_seconds:
|
if now - os.path.getctime(filepath) > expiration_seconds:
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
os.remove(filepath)
|
||||||
@@ -61,16 +53,12 @@ def cleanup_task():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Cleanup Error: {e}")
|
print(f"Cleanup Error: {e}")
|
||||||
|
|
||||||
time.sleep(60) # Run every minute
|
time.sleep(60)
|
||||||
|
|
||||||
# Start cleanup thread safely
|
|
||||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' or not os.environ.get('WERKZEUG_RUN_MAIN'):
|
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' or not os.environ.get('WERKZEUG_RUN_MAIN'):
|
||||||
# Simple check to try and avoid double threads in reloader, though not perfect
|
|
||||||
t = threading.Thread(target=cleanup_task, daemon=True)
|
t = threading.Thread(target=cleanup_task, daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
# --- Endpoint 1: Create Art (Optional: Embed Audio in it) ---
|
|
||||||
@app.route('/api/generate-art', methods=['POST'])
|
@app.route('/api/generate-art', methods=['POST'])
|
||||||
def generate_art():
|
def generate_art():
|
||||||
if 'audio' not in request.files:
|
if 'audio' not in request.files:
|
||||||
@@ -83,27 +71,19 @@ def generate_art():
|
|||||||
art_path = None
|
art_path = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. Save Audio
|
|
||||||
audio_path = save_upload(audio_file)
|
audio_path = save_upload(audio_file)
|
||||||
|
|
||||||
# 2. Generate Art
|
|
||||||
min_pixels = 0
|
min_pixels = 0
|
||||||
if should_embed:
|
if should_embed:
|
||||||
# Calculate required pixels: File Bytes * 8 (bits) / 3 (channels)
|
|
||||||
# Add 5% buffer for header and safety
|
|
||||||
file_size = os.path.getsize(audio_path)
|
file_size = os.path.getsize(audio_path)
|
||||||
min_pixels = int((file_size * 8 / 3) * 1.05)
|
min_pixels = int((file_size * 8 / 3) * 1.05)
|
||||||
|
|
||||||
art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels)
|
art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels)
|
||||||
|
|
||||||
# 3. If Embed requested, run Steganography immediately using the art as host
|
|
||||||
final_path = art_path
|
final_path = art_path
|
||||||
if should_embed:
|
if should_embed:
|
||||||
# art_path becomes the host, audio_path is the data
|
|
||||||
final_path = processor.encode_stego(audio_path, art_path)
|
final_path = processor.encode_stego(audio_path, art_path)
|
||||||
|
|
||||||
# If we created a new stego image, the pure art_path is intermediate (and audio is input)
|
|
||||||
# We can delete art_path now if it's different (it is)
|
|
||||||
if art_path != final_path:
|
if art_path != final_path:
|
||||||
try: os.remove(art_path)
|
try: os.remove(art_path)
|
||||||
except: pass
|
except: pass
|
||||||
@@ -115,14 +95,10 @@ def generate_art():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
finally:
|
finally:
|
||||||
# Cleanup Inputs
|
|
||||||
if audio_path and os.path.exists(audio_path):
|
if audio_path and os.path.exists(audio_path):
|
||||||
try: os.remove(audio_path)
|
try: os.remove(audio_path)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Endpoint 3: Steganography (Audio + Custom Host) ---
|
|
||||||
@app.route('/api/hide', methods=['POST'])
|
@app.route('/api/hide', methods=['POST'])
|
||||||
def hide_data():
|
def hide_data():
|
||||||
if 'data' not in request.files or 'host' not in request.files:
|
if 'data' not in request.files or 'host' not in request.files:
|
||||||
@@ -148,7 +124,6 @@ def hide_data():
|
|||||||
try: os.remove(host_path)
|
try: os.remove(host_path)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# --- Endpoint 4: Decode (Universal) ---
|
|
||||||
@app.route('/api/decode', methods=['POST'])
|
@app.route('/api/decode', methods=['POST'])
|
||||||
def decode():
|
def decode():
|
||||||
if 'image' not in request.files:
|
if 'image' not in request.files:
|
||||||
@@ -159,7 +134,6 @@ def decode():
|
|||||||
img_path = save_upload(request.files['image'])
|
img_path = save_upload(request.files['image'])
|
||||||
restored_path = processor.decode_image(img_path)
|
restored_path = processor.decode_image(img_path)
|
||||||
|
|
||||||
# Determine mimetype based on extension for browser friendliness
|
|
||||||
filename = os.path.basename(restored_path)
|
filename = os.path.basename(restored_path)
|
||||||
return send_file(restored_path, as_attachment=True, download_name=filename)
|
return send_file(restored_path, as_attachment=True, download_name=filename)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -171,13 +145,8 @@ def decode():
|
|||||||
try: os.remove(img_path)
|
try: os.remove(img_path)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# --- Endpoint 4: Visualizer SSE Stream ---
|
|
||||||
@app.route('/api/visualize', methods=['POST'])
|
@app.route('/api/visualize', methods=['POST'])
|
||||||
def visualize():
|
def visualize():
|
||||||
"""
|
|
||||||
SSE endpoint that streams the spectrogram generation process.
|
|
||||||
Returns step-by-step updates for visualization.
|
|
||||||
"""
|
|
||||||
if 'audio' not in request.files:
|
if 'audio' not in request.files:
|
||||||
return jsonify({"error": "No audio file provided"}), 400
|
return jsonify({"error": "No audio file provided"}), 400
|
||||||
|
|
||||||
@@ -196,56 +165,48 @@ def visualize():
|
|||||||
try:
|
try:
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
# Step 1: Audio loaded
|
|
||||||
yield f"data: {json.dumps({'step': 1, 'status': 'loading', 'message': 'Loading audio file...', 'progress': 10})}\n\n"
|
yield f"data: {json.dumps({'step': 1, 'status': 'loading', 'message': 'Loading audio file...', 'progress': 10})}\n\n"
|
||||||
time.sleep(0.8)
|
time.sleep(0.8)
|
||||||
|
|
||||||
yield f"data: {json.dumps({'step': 1, 'status': 'complete', 'message': f'Audio loaded: {audio_file.filename}', 'progress': 20, 'fileSize': file_size})}\n\n"
|
yield f"data: {json.dumps({'step': 1, 'status': 'complete', 'message': f'Audio loaded: {audio_file.filename}', 'progress': 20, 'fileSize': file_size})}\n\n"
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Step 2: Analyzing audio
|
|
||||||
yield f"data: {json.dumps({'step': 2, 'status': 'loading', 'message': 'Analyzing audio frequencies...', 'progress': 30})}\n\n"
|
yield f"data: {json.dumps({'step': 2, 'status': 'loading', 'message': 'Analyzing audio frequencies...', 'progress': 30})}\n\n"
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
|
|
||||||
yield f"data: {json.dumps({'step': 2, 'status': 'complete', 'message': 'Frequency analysis complete', 'progress': 40})}\n\n"
|
yield f"data: {json.dumps({'step': 2, 'status': 'complete', 'message': 'Frequency analysis complete', 'progress': 40})}\n\n"
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
# Step 3: Generating spectrogram
|
|
||||||
yield f"data: {json.dumps({'step': 3, 'status': 'loading', 'message': 'Generating spectrogram image...', 'progress': 50})}\n\n"
|
yield f"data: {json.dumps({'step': 3, 'status': 'loading', 'message': 'Generating spectrogram image...', 'progress': 50})}\n\n"
|
||||||
|
|
||||||
print(f"[VISUALIZE] Starting spectrogram generation for {audio_path}")
|
print(f"[VISUALIZE] Starting spectrogram generation for {audio_path}")
|
||||||
art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels)
|
art_path = processor.generate_spectrogram(audio_path, min_pixels=min_pixels)
|
||||||
print(f"[VISUALIZE] Spectrogram generated at {art_path}")
|
print(f"[VISUALIZE] Spectrogram generated at {art_path}")
|
||||||
|
|
||||||
# Read the spectrogram image and encode as base64
|
|
||||||
with open(art_path, 'rb') as img_file:
|
with open(art_path, 'rb') as img_file:
|
||||||
spectrogram_b64 = base64.b64encode(img_file.read()).decode('utf-8')
|
spectrogram_b64 = base64.b64encode(img_file.read()).decode('utf-8')
|
||||||
print(f"[VISUALIZE] Spectrogram base64 length: {len(spectrogram_b64)}")
|
print(f"[VISUALIZE] Spectrogram base64 length: {len(spectrogram_b64)}")
|
||||||
|
|
||||||
yield f"data: {json.dumps({'step': 3, 'status': 'complete', 'message': 'Spectrogram generated!', 'progress': 70, 'spectrogramImage': f'data:image/png;base64,{spectrogram_b64}'})}\n\n"
|
yield f"data: {json.dumps({'step': 3, 'status': 'complete', 'message': 'Spectrogram generated!', 'progress': 70, 'spectrogramImage': f'data:image/png;base64,{spectrogram_b64}'})}\n\n"
|
||||||
print("[VISUALIZE] Sent spectrogram image")
|
print("[VISUALIZE] Sent spectrogram image")
|
||||||
time.sleep(2.0) # Pause to let user see the spectrogram
|
time.sleep(2.0)
|
||||||
|
|
||||||
# Step 4: Embedding audio
|
|
||||||
yield f"data: {json.dumps({'step': 4, 'status': 'loading', 'message': 'Embedding audio into image (LSB steganography)...', 'progress': 80})}\n\n"
|
yield f"data: {json.dumps({'step': 4, 'status': 'loading', 'message': 'Embedding audio into image (LSB steganography)...', 'progress': 80})}\n\n"
|
||||||
|
|
||||||
final_path = processor.encode_stego(audio_path, art_path)
|
final_path = processor.encode_stego(audio_path, art_path)
|
||||||
|
|
||||||
# Read the final image and encode as base64
|
|
||||||
with open(final_path, 'rb') as img_file:
|
with open(final_path, 'rb') as img_file:
|
||||||
final_b64 = base64.b64encode(img_file.read()).decode('utf-8')
|
final_b64 = base64.b64encode(img_file.read()).decode('utf-8')
|
||||||
|
|
||||||
yield f"data: {json.dumps({'step': 4, 'status': 'complete', 'message': 'Audio embedded successfully!', 'progress': 95, 'finalImage': f'data:image/png;base64,{final_b64}'})}\n\n"
|
yield f"data: {json.dumps({'step': 4, 'status': 'complete', 'message': 'Audio embedded successfully!', 'progress': 95, 'finalImage': f'data:image/png;base64,{final_b64}'})}\n\n"
|
||||||
time.sleep(2.0) # Pause to let user see the final image
|
time.sleep(2.0)
|
||||||
|
|
||||||
# Step 5: Complete - send the result URL
|
|
||||||
result_id = os.path.basename(final_path)
|
result_id = os.path.basename(final_path)
|
||||||
yield f"data: {json.dumps({'step': 5, 'status': 'complete', 'message': 'Process complete!', 'progress': 100, 'resultId': result_id})}\n\n"
|
yield f"data: {json.dumps({'step': 5, 'status': 'complete', 'message': 'Process complete!', 'progress': 100, 'resultId': result_id})}\n\n"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield f"data: {json.dumps({'step': 0, 'status': 'error', 'message': str(e), 'progress': 0})}\n\n"
|
yield f"data: {json.dumps({'step': 0, 'status': 'error', 'message': str(e), 'progress': 0})}\n\n"
|
||||||
finally:
|
finally:
|
||||||
# Clean up intermediate files (but keep final)
|
|
||||||
if art_path and art_path != final_path and os.path.exists(art_path):
|
if art_path and art_path != final_path and os.path.exists(art_path):
|
||||||
try: os.remove(art_path)
|
try: os.remove(art_path)
|
||||||
except: pass
|
except: pass
|
||||||
@@ -265,16 +226,12 @@ def visualize():
|
|||||||
except: pass
|
except: pass
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/result/<result_id>', methods=['GET'])
|
@app.route('/api/result/<result_id>', methods=['GET'])
|
||||||
def get_result(result_id):
|
def get_result(result_id):
|
||||||
"""Serve the result image by ID."""
|
|
||||||
result_path = os.path.join(app.config['UPLOAD_FOLDER'], result_id)
|
result_path = os.path.join(app.config['UPLOAD_FOLDER'], result_id)
|
||||||
if os.path.exists(result_path):
|
if os.path.exists(result_path):
|
||||||
return send_file(result_path, mimetype='image/png', as_attachment=False)
|
return send_file(result_path, mimetype='image/png', as_attachment=False)
|
||||||
return jsonify({"error": "Result not found"}), 404
|
return jsonify({"error": "Result not found"}), 404
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Threaded=True is important for processing images without blocking
|
|
||||||
app.run(debug=True, port=5000, threaded=True)
|
app.run(debug=True, port=5000, threaded=True)
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import numpy as np
|
|||||||
import librosa
|
import librosa
|
||||||
import librosa.display
|
import librosa.display
|
||||||
import matplotlib
|
import matplotlib
|
||||||
# Set backend to Agg (Anti-Grain Geometry) to render without a GUI (essential for servers)
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
# --- Constants ---
|
|
||||||
MAX_MB = 40
|
MAX_MB = 40
|
||||||
SIG_SHIFT = b'B2I!'
|
SIG_SHIFT = b'B2I!'
|
||||||
SIG_STEGO = b'B2S!'
|
SIG_STEGO = b'B2S!'
|
||||||
@@ -25,7 +23,6 @@ class AudioImageProcessor:
|
|||||||
os.makedirs(upload_folder, exist_ok=True)
|
os.makedirs(upload_folder, exist_ok=True)
|
||||||
|
|
||||||
def _get_bytes(self, path):
|
def _get_bytes(self, path):
|
||||||
"""Helper to safely read bytes"""
|
|
||||||
if os.path.getsize(path) > (MAX_MB * 1024 * 1024):
|
if os.path.getsize(path) > (MAX_MB * 1024 * 1024):
|
||||||
raise ValueError("File too large (Max 40MB)")
|
raise ValueError("File too large (Max 40MB)")
|
||||||
with open(path, 'rb') as f:
|
with open(path, 'rb') as f:
|
||||||
@@ -36,9 +33,7 @@ class AudioImageProcessor:
|
|||||||
ext_bytes = ext.encode('utf-8')
|
ext_bytes = ext.encode('utf-8')
|
||||||
return struct.pack(HEADER_FMT, signature, file_size, len(ext_bytes)) + ext_bytes
|
return struct.pack(HEADER_FMT, signature, file_size, len(ext_bytes)) + ext_bytes
|
||||||
|
|
||||||
# --- Feature 1: Spectrogram Art ---
|
|
||||||
def generate_spectrogram(self, audio_path, min_pixels=0):
|
def generate_spectrogram(self, audio_path, min_pixels=0):
|
||||||
"""Generates a visual spectrogram from audio."""
|
|
||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
import torchaudio
|
import torchaudio
|
||||||
@@ -48,13 +43,10 @@ class AudioImageProcessor:
|
|||||||
|
|
||||||
if has_torch and torch.cuda.is_available():
|
if has_torch and torch.cuda.is_available():
|
||||||
try:
|
try:
|
||||||
# GPU Accelerated Path
|
|
||||||
device = "cuda"
|
device = "cuda"
|
||||||
waveform, sr = torchaudio.load(audio_path)
|
waveform, sr = torchaudio.load(audio_path)
|
||||||
waveform = waveform.to(device)
|
waveform = waveform.to(device)
|
||||||
|
|
||||||
# Create transformation
|
|
||||||
# Mimic librosa defaults roughly: n_fft=2048, hop_length=512
|
|
||||||
n_fft = 2048
|
n_fft = 2048
|
||||||
win_length = n_fft
|
win_length = n_fft
|
||||||
hop_length = 512
|
hop_length = 512
|
||||||
@@ -72,17 +64,13 @@ class AudioImageProcessor:
|
|||||||
S = mel_spectrogram(waveform)
|
S = mel_spectrogram(waveform)
|
||||||
S_dB = torchaudio.transforms.AmplitudeToDB()(S)
|
S_dB = torchaudio.transforms.AmplitudeToDB()(S)
|
||||||
|
|
||||||
# Back to CPU for plotting
|
S_dB = S_dB.cpu().numpy()[0]
|
||||||
S_dB = S_dB.cpu().numpy()[0] # Take first channel
|
|
||||||
# Librosa display expects numpy
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Fallback to CPU/Librosa if any error occurs
|
|
||||||
print(f"GPU processing failed, falling back to CPU: {e}")
|
print(f"GPU processing failed, falling back to CPU: {e}")
|
||||||
return self._generate_spectrogram_cpu(audio_path, min_pixels)
|
return self._generate_spectrogram_cpu(audio_path, min_pixels)
|
||||||
else:
|
else:
|
||||||
return self._generate_spectrogram_cpu(audio_path, min_pixels)
|
return self._generate_spectrogram_cpu(audio_path, min_pixels)
|
||||||
|
|
||||||
# Plotting (Common)
|
|
||||||
return self._plot_spectrogram(S_dB, sr, min_pixels)
|
return self._plot_spectrogram(S_dB, sr, min_pixels)
|
||||||
|
|
||||||
def _generate_spectrogram_cpu(self, audio_path, min_pixels=0):
|
def _generate_spectrogram_cpu(self, audio_path, min_pixels=0):
|
||||||
@@ -92,44 +80,31 @@ class AudioImageProcessor:
|
|||||||
return self._plot_spectrogram(S_dB, sr, min_pixels)
|
return self._plot_spectrogram(S_dB, sr, min_pixels)
|
||||||
|
|
||||||
def _plot_spectrogram(self, S_dB, sr, min_pixels=0):
|
def _plot_spectrogram(self, S_dB, sr, min_pixels=0):
|
||||||
# Calculate DPI dynamically to ensure we have enough pixels for steganography
|
|
||||||
dpi = 300
|
dpi = 300
|
||||||
if min_pixels > 0:
|
if min_pixels > 0:
|
||||||
# Figure is 12x6 inches. Area = 72 sq inches.
|
|
||||||
# Total Pixels = 72 * dpi^2
|
|
||||||
required_dpi = math.ceil((min_pixels / 72) ** 0.5)
|
required_dpi = math.ceil((min_pixels / 72) ** 0.5)
|
||||||
# Add a small buffer
|
|
||||||
dpi = max(dpi, int(required_dpi * 1.05))
|
dpi = max(dpi, int(required_dpi * 1.05))
|
||||||
|
|
||||||
# Use exact dimensions without margins
|
|
||||||
width_in = 12
|
width_in = 12
|
||||||
height_in = 6
|
height_in = 6
|
||||||
fig = plt.figure(figsize=(width_in, height_in))
|
fig = plt.figure(figsize=(width_in, height_in))
|
||||||
|
|
||||||
# Add axes covering the entire figure [left, bottom, width, height]
|
|
||||||
ax = plt.axes([0, 0, 1, 1], frameon=False)
|
ax = plt.axes([0, 0, 1, 1], frameon=False)
|
||||||
ax.set_axis_off()
|
ax.set_axis_off()
|
||||||
|
|
||||||
# 'magma' is a nice default
|
|
||||||
librosa.display.specshow(S_dB, sr=sr, fmax=8000, cmap='magma', ax=ax)
|
librosa.display.specshow(S_dB, sr=sr, fmax=8000, cmap='magma', ax=ax)
|
||||||
|
|
||||||
output_path = os.path.join(self.upload_folder, f"art_{int(time.time())}.png")
|
output_path = os.path.join(self.upload_folder, f"art_{int(time.time())}.png")
|
||||||
|
|
||||||
# specific DPI, no bbox_inches='tight' (which shrinks the image)
|
|
||||||
plt.savefig(output_path, dpi=dpi)
|
plt.savefig(output_path, dpi=dpi)
|
||||||
plt.close()
|
plt.close()
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Feature 3: Steganography (Embed in Host) ---
|
|
||||||
def encode_stego(self, data_path, host_path):
|
def encode_stego(self, data_path, host_path):
|
||||||
# 1. Prepare Data
|
|
||||||
file_data = self._get_bytes(data_path)
|
file_data = self._get_bytes(data_path)
|
||||||
header = self._create_header(SIG_STEGO, len(file_data), data_path)
|
header = self._create_header(SIG_STEGO, len(file_data), data_path)
|
||||||
payload_bits = np.unpackbits(np.frombuffer(header + file_data, dtype=np.uint8))
|
payload_bits = np.unpackbits(np.frombuffer(header + file_data, dtype=np.uint8))
|
||||||
|
|
||||||
# 2. Prepare Host
|
|
||||||
host = Image.open(host_path).convert('RGB')
|
host = Image.open(host_path).convert('RGB')
|
||||||
host_arr = np.array(host)
|
host_arr = np.array(host)
|
||||||
flat_host = host_arr.flatten()
|
flat_host = host_arr.flatten()
|
||||||
@@ -137,7 +112,6 @@ class AudioImageProcessor:
|
|||||||
if len(payload_bits) > len(flat_host):
|
if len(payload_bits) > len(flat_host):
|
||||||
raise ValueError(f"Host image too small. Need {len(payload_bits)/3/1e6:.2f} MP.")
|
raise ValueError(f"Host image too small. Need {len(payload_bits)/3/1e6:.2f} MP.")
|
||||||
|
|
||||||
# 3. Embed (LSB)
|
|
||||||
padded_bits = np.pad(payload_bits, (0, len(flat_host) - len(payload_bits)), 'constant')
|
padded_bits = np.pad(payload_bits, (0, len(flat_host) - len(payload_bits)), 'constant')
|
||||||
embedded_flat = (flat_host & 0xFE) + padded_bits
|
embedded_flat = (flat_host & 0xFE) + padded_bits
|
||||||
|
|
||||||
@@ -147,19 +121,16 @@ class AudioImageProcessor:
|
|||||||
embedded_img.save(output_path, "PNG")
|
embedded_img.save(output_path, "PNG")
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
# --- Feature 4: Universal Decoder ---
|
|
||||||
def decode_image(self, image_path):
|
def decode_image(self, image_path):
|
||||||
img = Image.open(image_path).convert('RGB')
|
img = Image.open(image_path).convert('RGB')
|
||||||
flat_bytes = np.array(img).flatten()
|
flat_bytes = np.array(img).flatten()
|
||||||
|
|
||||||
# Strategy A: Check for Shift Signature (Raw Bytes)
|
|
||||||
try:
|
try:
|
||||||
sig = struct.unpack('>4s', flat_bytes[:4])[0]
|
sig = struct.unpack('>4s', flat_bytes[:4])[0]
|
||||||
if sig == SIG_SHIFT:
|
if sig == SIG_SHIFT:
|
||||||
return self._extract(flat_bytes, image_path, is_bits=False)
|
return self._extract(flat_bytes, image_path, is_bits=False)
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# Strategy B: Check for Stego Signature (LSB)
|
|
||||||
try:
|
try:
|
||||||
sample_bytes = np.packbits(flat_bytes[:300] & 1)
|
sample_bytes = np.packbits(flat_bytes[:300] & 1)
|
||||||
sig = struct.unpack('>4s', sample_bytes[:4])[0]
|
sig = struct.unpack('>4s', sample_bytes[:4])[0]
|
||||||
|
|||||||
@@ -45,25 +45,20 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Handle success
|
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
const downloadUrl = URL.createObjectURL(blob);
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
// Detect if it's audio
|
|
||||||
const contentDisposition = res.headers.get('Content-Disposition') || '';
|
const contentDisposition = res.headers.get('Content-Disposition') || '';
|
||||||
let fileName = 'decoded_file';
|
let fileName = 'decoded_file';
|
||||||
const match = contentDisposition.match(/filename="?(.+)"?/);
|
const match = contentDisposition.match(/filename="?(.+)"?/);
|
||||||
if (match && match[1]) fileName = match[1];
|
if (match && match[1]) fileName = match[1];
|
||||||
|
|
||||||
if (fileName.match(/\.(mp3|aac|wav|ogg|m4a)$/i)) {
|
if (fileName.match(/\.(mp3|aac|wav|ogg|m4a)$/i)) {
|
||||||
// Success! Dispatch the URL and image to parent for playing
|
|
||||||
// We'll pass the blob URL and the original file (to show cover art)
|
|
||||||
dispatch('decodesuccess', {
|
dispatch('decodesuccess', {
|
||||||
audioUrl: downloadUrl,
|
audioUrl: downloadUrl,
|
||||||
imageFile: decodeImageFile
|
imageFile: decodeImageFile
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// For non-audio (or unknown), auto-download as before
|
|
||||||
triggerDownload(downloadUrl, fileName);
|
triggerDownload(downloadUrl, fileName);
|
||||||
URL.revokeObjectURL(downloadUrl);
|
URL.revokeObjectURL(downloadUrl);
|
||||||
}
|
}
|
||||||
@@ -81,7 +76,6 @@
|
|||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
// Revoke the URL after download to free up memory
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- Helper for inputs -->
|
|
||||||
{#each ['Payload File (Secret)', 'Host Image (Cover)'] as label, i}
|
{#each ['Payload File (Secret)', 'Host Image (Cover)'] as label, i}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||||
@@ -89,7 +88,6 @@
|
|||||||
stegoDataFile = file;
|
stegoDataFile = file;
|
||||||
} else {
|
} else {
|
||||||
stegoHostFile = file;
|
stegoHostFile = file;
|
||||||
// Calculate MP
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Audio Player State
|
|
||||||
let audioElement: HTMLAudioElement | undefined = $state();
|
let audioElement: HTMLAudioElement | undefined = $state();
|
||||||
let isPaused = $state(true);
|
let isPaused = $state(true);
|
||||||
let currentTime = $state(0);
|
let currentTime = $state(0);
|
||||||
@@ -57,7 +56,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="mt-10 space-y-6">
|
<section class="mt-10 space-y-6">
|
||||||
<!-- Audio Art / Encoder Results -->
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
{#if artResultUrl}
|
{#if artResultUrl}
|
||||||
<div
|
<div
|
||||||
@@ -116,7 +114,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decoded Audio Player -->
|
|
||||||
{#if decodedAudioUrl}
|
{#if decodedAudioUrl}
|
||||||
<div class="glass-panel p-6" transition:slide>
|
<div class="glass-panel p-6" transition:slide>
|
||||||
<h3 class="section-title mb-6 flex items-center gap-2 text-emerald-400">
|
<h3 class="section-title mb-6 flex items-center gap-2 text-emerald-400">
|
||||||
@@ -125,7 +122,6 @@
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6 md:flex-row md:items-center">
|
<div class="flex flex-col gap-6 md:flex-row md:items-center">
|
||||||
<!-- Cover Art -->
|
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<div
|
<div
|
||||||
class="h-32 w-32 overflow-hidden rounded-xl border border-white/10 bg-(--bg-deep) md:h-40 md:w-40"
|
class="h-32 w-32 overflow-hidden rounded-xl border border-white/10 bg-(--bg-deep) md:h-40 md:w-40"
|
||||||
@@ -140,7 +136,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Controls -->
|
|
||||||
<div class="flex flex-1 flex-col justify-center">
|
<div class="flex flex-1 flex-col justify-center">
|
||||||
<audio
|
<audio
|
||||||
src={decodedAudioUrl}
|
src={decodedAudioUrl}
|
||||||
@@ -166,7 +161,6 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Progress -->
|
|
||||||
<div class="flex flex-1 flex-col gap-1">
|
<div class="flex flex-1 flex-col gap-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
let audioFile: File | null = $state(null);
|
let audioFile: File | null = $state(null);
|
||||||
let embedAudio = $state(true);
|
let embedAudio = $state(true);
|
||||||
|
|
||||||
// Helper functions passed from parent or redefined?
|
|
||||||
// Redefining helper for self-contained component
|
|
||||||
function handleFileDrop(e: DragEvent, setter: (f: File) => void) {
|
function handleFileDrop(e: DragEvent, setter: (f: File) => void) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.dataTransfer?.files && e.dataTransfer.files[0]) {
|
if (e.dataTransfer?.files && e.dataTransfer.files[0]) {
|
||||||
@@ -32,12 +30,6 @@
|
|||||||
formData.append('embed', embedAudio.toString());
|
formData.append('embed', embedAudio.toString());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We need API_URL. We can pass it as prop or assume generic relative path if served from same origin
|
|
||||||
// The original used absolute http://127.0.0.1:5000/api
|
|
||||||
// Let's assume the parent handles the API_URL or we import it.
|
|
||||||
// For now, hardcode or use relative '/api' if proxy is set up?
|
|
||||||
// User had 'http://127.0.0.1:5000/api'.
|
|
||||||
|
|
||||||
const res = await fetch('http://127.0.0.1:5000/api/generate-art', {
|
const res = await fetch('http://127.0.0.1:5000/api/generate-art', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
@@ -90,7 +82,6 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<!-- File Input -->
|
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer rounded-lg border-2 border-dashed border-(--border-subtle) p-8 text-center transition-colors hover:border-(--primary) {loading
|
class="cursor-pointer rounded-lg border-2 border-dashed border-(--border-subtle) p-8 text-center transition-colors hover:border-(--primary) {loading
|
||||||
? 'pointer-events-none opacity-50'
|
? 'pointer-events-none opacity-50'
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
let artResultUrl = '';
|
let artResultUrl = '';
|
||||||
let encoderResultUrl = '';
|
let encoderResultUrl = '';
|
||||||
|
|
||||||
// Decoder State
|
|
||||||
let decodedAudioUrl: string | null = null;
|
let decodedAudioUrl: string | null = null;
|
||||||
let decodedImageFile: File | null = null;
|
let decodedImageFile: File | null = null;
|
||||||
|
|
||||||
@@ -92,9 +91,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="grid flex-1 gap-8 md:grid-cols-2">
|
<div class="grid flex-1 gap-8 md:grid-cols-2">
|
||||||
<!-- LEFT COLUMN: CREATION TOOLS -->
|
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- TOOL 1: SPECTROGRAM ART -->
|
|
||||||
<SpectrogramTool
|
<SpectrogramTool
|
||||||
{loading}
|
{loading}
|
||||||
on:start={handleStart}
|
on:start={handleStart}
|
||||||
@@ -103,7 +100,6 @@
|
|||||||
on:complete={handleArtComplete}
|
on:complete={handleArtComplete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- TOOL 2: ENCODER (Shift & Hide) -->
|
|
||||||
<EncoderTool
|
<EncoderTool
|
||||||
{loading}
|
{loading}
|
||||||
on:start={handleStart}
|
on:start={handleStart}
|
||||||
@@ -113,9 +109,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT COLUMN: RESULTS & DECODER -->
|
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<!-- TOOL 3: UNIVERSAL DECODER -->
|
|
||||||
<DecoderTool
|
<DecoderTool
|
||||||
{loading}
|
{loading}
|
||||||
on:start={handleStart}
|
on:start={handleStart}
|
||||||
@@ -124,7 +118,6 @@
|
|||||||
on:decodesuccess={handleDecodeSuccess}
|
on:decodesuccess={handleDecodeSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- RESULTS PREVIEW AREA -->
|
|
||||||
<ResultPreview {artResultUrl} {encoderResultUrl} {decodedAudioUrl} {decodedImageFile} />
|
<ResultPreview {artResultUrl} {encoderResultUrl} {decodedAudioUrl} {decodedImageFile} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container py-8 sm:py-12">
|
<div class="container py-8 sm:py-12">
|
||||||
<!-- Header -->
|
|
||||||
<div class="mb-8 text-center sm:mb-12">
|
<div class="mb-8 text-center sm:mb-12">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
@@ -28,9 +27,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<article class="mx-auto prose max-w-4xl prose-invert">
|
<article class="mx-auto prose max-w-4xl prose-invert">
|
||||||
<!-- Section 1 -->
|
|
||||||
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
||||||
<h2 class="flex items-center gap-2 text-xl font-bold text-(--primary) sm:gap-3 sm:text-2xl">
|
<h2 class="flex items-center gap-2 text-xl font-bold text-(--primary) sm:gap-3 sm:text-2xl">
|
||||||
<Icon icon="heroicons:cube-transparent" class="h-6 w-6 sm:h-7 sm:w-7" />
|
<Icon icon="heroicons:cube-transparent" class="h-6 w-6 sm:h-7 sm:w-7" />
|
||||||
@@ -87,7 +84,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section 2 -->
|
|
||||||
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
||||||
<h2 class="flex items-center gap-3 text-2xl font-bold text-pink-500">
|
<h2 class="flex items-center gap-3 text-2xl font-bold text-pink-500">
|
||||||
<Icon icon="heroicons:lock-closed" class="h-7 w-7" />
|
<Icon icon="heroicons:lock-closed" class="h-7 w-7" />
|
||||||
@@ -152,7 +148,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section 3 -->
|
|
||||||
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
||||||
<h2 class="flex items-center gap-3 text-2xl font-bold text-purple-400">
|
<h2 class="flex items-center gap-3 text-2xl font-bold text-purple-400">
|
||||||
<Icon icon="heroicons:arrows-pointing-out" class="h-7 w-7" />
|
<Icon icon="heroicons:arrows-pointing-out" class="h-7 w-7" />
|
||||||
@@ -201,7 +196,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section 4 -->
|
|
||||||
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
||||||
<h2 class="flex items-center gap-3 text-2xl font-bold text-emerald-400">
|
<h2 class="flex items-center gap-3 text-2xl font-bold text-emerald-400">
|
||||||
<Icon icon="heroicons:musical-note" class="h-7 w-7" />
|
<Icon icon="heroicons:musical-note" class="h-7 w-7" />
|
||||||
@@ -252,7 +246,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section 5 -->
|
|
||||||
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
<section class="glass-panel mb-6 p-4 sm:mb-8 sm:p-6 md:p-8">
|
||||||
<h2 class="flex items-center gap-3 text-2xl font-bold text-amber-400">
|
<h2 class="flex items-center gap-3 text-2xl font-bold text-amber-400">
|
||||||
<Icon icon="heroicons:cog-6-tooth" class="h-7 w-7" />
|
<Icon icon="heroicons:cog-6-tooth" class="h-7 w-7" />
|
||||||
@@ -319,7 +312,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<!-- Back to App Button -->
|
|
||||||
<div class="mt-8 text-center sm:mt-12">
|
<div class="mt-8 text-center sm:mt-12">
|
||||||
<a href="/" class="btn-primary inline-flex items-center gap-2">
|
<a href="/" class="btn-primary inline-flex items-center gap-2">
|
||||||
<Icon icon="heroicons:arrow-left" class="h-5 w-5" />
|
<Icon icon="heroicons:arrow-left" class="h-5 w-5" />
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import Icon from '@iconify/svelte';
|
import Icon from '@iconify/svelte';
|
||||||
import { fade, scale } from 'svelte/transition';
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
|
||||||
// State (Svelte 5 runes)
|
|
||||||
let audioFile: File | null = $state(null);
|
let audioFile: File | null = $state(null);
|
||||||
let isProcessing = $state(false);
|
let isProcessing = $state(false);
|
||||||
let currentStep = $state(0);
|
let currentStep = $state(0);
|
||||||
@@ -10,11 +9,9 @@
|
|||||||
let resultId: string | null = $state(null);
|
let resultId: string | null = $state(null);
|
||||||
let error: string | null = $state(null);
|
let error: string | null = $state(null);
|
||||||
|
|
||||||
// Image previews
|
|
||||||
let spectrogramImage: string | null = $state(null);
|
let spectrogramImage: string | null = $state(null);
|
||||||
let finalImage: string | null = $state(null);
|
let finalImage: string | null = $state(null);
|
||||||
|
|
||||||
// Audio Visualizer State
|
|
||||||
let audioUrl: string | null = $state(null);
|
let audioUrl: string | null = $state(null);
|
||||||
let audioElement: HTMLAudioElement | undefined = $state();
|
let audioElement: HTMLAudioElement | undefined = $state();
|
||||||
let canvasElement: HTMLCanvasElement | undefined = $state();
|
let canvasElement: HTMLCanvasElement | undefined = $state();
|
||||||
@@ -44,7 +41,6 @@
|
|||||||
analyser.fftSize = 256;
|
analyser.fftSize = 256;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create source only once per audio element/context session or reuse
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
try {
|
try {
|
||||||
source = audioContext.createMediaElementSource(audioElement);
|
source = audioContext.createMediaElementSource(audioElement);
|
||||||
@@ -61,7 +57,7 @@
|
|||||||
function drawVisualizer() {
|
function drawVisualizer() {
|
||||||
if (!canvasElement || !analyser) return;
|
if (!canvasElement || !analyser) return;
|
||||||
|
|
||||||
analyser.fftSize = 2048; // Higher resolution for waveform
|
analyser.fftSize = 2048;
|
||||||
const bufferLength = analyser.frequencyBinCount;
|
const bufferLength = analyser.frequencyBinCount;
|
||||||
const dataArray = new Uint8Array(bufferLength);
|
const dataArray = new Uint8Array(bufferLength);
|
||||||
const ctx = canvasElement.getContext('2d');
|
const ctx = canvasElement.getContext('2d');
|
||||||
@@ -74,21 +70,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
animationFrame = requestAnimationFrame(draw);
|
animationFrame = requestAnimationFrame(draw);
|
||||||
analyser!.getByteTimeDomainData(dataArray); // Use TimeDomain for waveform
|
analyser!.getByteTimeDomainData(dataArray);
|
||||||
|
|
||||||
const width = canvasElement!.width;
|
const width = canvasElement!.width;
|
||||||
const height = canvasElement!.height;
|
const height = canvasElement!.height;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
// Draw smooth line waveform
|
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
|
|
||||||
// Create gradient
|
|
||||||
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||||
gradient.addColorStop(0, '#a855f7'); // Purple
|
gradient.addColorStop(0, '#a855f7');
|
||||||
gradient.addColorStop(0.5, '#ec4899'); // Pink
|
gradient.addColorStop(0.5, '#ec4899');
|
||||||
gradient.addColorStop(1, '#a855f7'); // Purple
|
gradient.addColorStop(1, '#a855f7');
|
||||||
ctx.strokeStyle = gradient;
|
ctx.strokeStyle = gradient;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -114,7 +108,6 @@
|
|||||||
ctx.lineTo(width, height / 2);
|
ctx.lineTo(width, height / 2);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Optional: Draw a center line for reference
|
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
@@ -140,8 +133,6 @@
|
|||||||
|
|
||||||
isProcessing = true;
|
isProcessing = true;
|
||||||
|
|
||||||
// Initialize and play audio
|
|
||||||
// Small timeout to ensure DOM elements are rendered
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (audioElement && audioContext?.state === 'suspended') {
|
if (audioElement && audioContext?.state === 'suspended') {
|
||||||
audioContext.resume();
|
audioContext.resume();
|
||||||
@@ -183,9 +174,8 @@
|
|||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
// Process complete SSE messages (they end with \n\n)
|
|
||||||
const messages = buffer.split('\n\n');
|
const messages = buffer.split('\n\n');
|
||||||
buffer = messages.pop() || ''; // Keep the incomplete last part
|
buffer = messages.pop() || '';
|
||||||
|
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
const lines = message.split('\n');
|
const lines = message.split('\n');
|
||||||
@@ -196,7 +186,6 @@
|
|||||||
currentStep = data.step;
|
currentStep = data.step;
|
||||||
progress = data.progress;
|
progress = data.progress;
|
||||||
|
|
||||||
// Capture images
|
|
||||||
if (data.spectrogramImage) {
|
if (data.spectrogramImage) {
|
||||||
spectrogramImage = data.spectrogramImage;
|
spectrogramImage = data.spectrogramImage;
|
||||||
}
|
}
|
||||||
@@ -236,10 +225,6 @@
|
|||||||
}
|
}
|
||||||
if (animationFrame) cancelAnimationFrame(animationFrame);
|
if (animationFrame) cancelAnimationFrame(animationFrame);
|
||||||
|
|
||||||
// Don't nullify audioContext to reuse it, or close it if you prefer
|
|
||||||
// source = null; // Source node usually stays valid for the context?
|
|
||||||
// Actually, if we remove audio element, we might need to recreate source next time.
|
|
||||||
// For simplicity, we just stop playing. Since audioFile becomes null, the audio element is removed/replaced.
|
|
||||||
source = null;
|
source = null;
|
||||||
|
|
||||||
audioFile = null;
|
audioFile = null;
|
||||||
@@ -262,7 +247,6 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="container py-8 sm:py-12">
|
<div class="container py-8 sm:py-12">
|
||||||
<!-- Header -->
|
|
||||||
<div class="mb-8 text-center sm:mb-12">
|
<div class="mb-8 text-center sm:mb-12">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
@@ -279,9 +263,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="mx-auto max-w-4xl space-y-6">
|
<div class="mx-auto max-w-4xl space-y-6">
|
||||||
<!-- Upload Section (only visible when no images and not processing) -->
|
|
||||||
{#if !spectrogramImage && !finalImage && !isProcessing}
|
{#if !spectrogramImage && !finalImage && !isProcessing}
|
||||||
<div class="glass-panel p-6 sm:p-8" transition:fade>
|
<div class="glass-panel p-6 sm:p-8" transition:fade>
|
||||||
<h2 class="section-title mb-4 text-(--primary)">
|
<h2 class="section-title mb-4 text-(--primary)">
|
||||||
@@ -317,10 +299,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Progress Bar & Audio Visualizer (visible during processing) -->
|
|
||||||
{#if isProcessing}
|
{#if isProcessing}
|
||||||
<div class="space-y-6" transition:fade>
|
<div class="space-y-6" transition:fade>
|
||||||
<!-- Audio Visualizer -->
|
|
||||||
{#if audioUrl}
|
{#if audioUrl}
|
||||||
<div class="glass-panel relative overflow-hidden p-0">
|
<div class="glass-panel relative overflow-hidden p-0">
|
||||||
<canvas
|
<canvas
|
||||||
@@ -329,16 +309,9 @@
|
|||||||
height="120"
|
height="120"
|
||||||
class="h-32 w-full object-cover opacity-80"
|
class="h-32 w-full object-cover opacity-80"
|
||||||
></canvas>
|
></canvas>
|
||||||
<audio
|
<audio bind:this={audioElement} src={audioUrl} class="hidden" onended={() => {}}
|
||||||
bind:this={audioElement}
|
|
||||||
src={audioUrl}
|
|
||||||
class="hidden"
|
|
||||||
onended={() => {
|
|
||||||
/* Optional: handle end */
|
|
||||||
}}
|
|
||||||
></audio>
|
></audio>
|
||||||
|
|
||||||
<!-- Overlay Info -->
|
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-3 left-6 flex items-center gap-2 text-sm font-medium text-white/80"
|
class="absolute bottom-3 left-6 flex items-center gap-2 text-sm font-medium text-white/80"
|
||||||
>
|
>
|
||||||
@@ -375,7 +348,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Image Preview (visible when images exist or processing) -->
|
|
||||||
{#if spectrogramImage || finalImage || isProcessing}
|
{#if spectrogramImage || finalImage || isProcessing}
|
||||||
<div class="glass-panel p-6 sm:p-8" transition:scale>
|
<div class="glass-panel p-6 sm:p-8" transition:scale>
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
@@ -395,7 +367,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<!-- Spectrogram Preview -->
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class="absolute -top-3 left-4 z-10 rounded-full border border-pink-500/30 bg-(--bg-card) px-3 py-1 text-xs font-semibold text-pink-500"
|
class="absolute -top-3 left-4 z-10 rounded-full border border-pink-500/30 bg-(--bg-card) px-3 py-1 text-xs font-semibold text-pink-500"
|
||||||
@@ -427,7 +398,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Final Image Preview -->
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div
|
<div
|
||||||
class="absolute -top-3 left-4 z-10 rounded-full border border-emerald-500/30 bg-(--bg-card) px-3 py-1 text-xs font-semibold text-emerald-400"
|
class="absolute -top-3 left-4 z-10 rounded-full border border-emerald-500/30 bg-(--bg-card) px-3 py-1 text-xs font-semibold text-emerald-400"
|
||||||
@@ -472,7 +442,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download button when complete -->
|
|
||||||
{#if resultId && !isProcessing}
|
{#if resultId && !isProcessing}
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<a
|
<a
|
||||||
@@ -488,7 +457,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Error -->
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-red-400"
|
class="rounded-lg border border-red-500/30 bg-red-500/10 p-4 text-red-400"
|
||||||
@@ -509,7 +477,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Back Button -->
|
|
||||||
<div class="mt-8 text-center sm:mt-12">
|
<div class="mt-8 text-center sm:mt-12">
|
||||||
<a href="/" class="btn-primary inline-flex items-center gap-2">
|
<a href="/" class="btn-primary inline-flex items-center gap-2">
|
||||||
<Icon icon="heroicons:arrow-left" class="h-5 w-5" />
|
<Icon icon="heroicons:arrow-left" class="h-5 w-5" />
|
||||||
@@ -519,7 +486,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Scanline reveal animation for spectrogram */
|
|
||||||
.image-reveal {
|
.image-reveal {
|
||||||
animation: revealFromTop 1s ease-out forwards;
|
animation: revealFromTop 1s ease-out forwards;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import adapter from '@sveltejs/adapter-static';
|
import adapter from '@sveltejs/adapter-static';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
|
||||||
const config = {
|
const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
kit: {
|
kit: {
|
||||||
|
|||||||
Reference in New Issue
Block a user