Repository navigation
Expand file tree
/
Copy pathimage_extender.py
More file actions
5776 lines (4790 loc) · 224 KB
/
Copy pathimage_extender.py
File metadata and controls
5776 lines (4790 loc) · 224 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
IMAGE_EXTENDER_v0_1_1 - 01022026 Local Version
This is a local Python adaptation of the original Google Colab notebook.
It provides the same functionality but runs locally with a tkinter GUI.
"""
import os
import sys
import base64
import json
import re
import ast
import time
import threading
import random
import datetime
import csv
import numpy as np
import requests
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import io
import tempfile
import warnings
warnings.filterwarnings('ignore')
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
# For audio processing
try:
from pydub import AudioSegment
from pydub.playback import play as pydub_play
import soundfile as sf
import librosa
from scipy.signal import hilbert, butter, sosfiltfilt, sosfilt, iirpeak, lfilter, windows
import scipy.signal as signal
import pyloudnorm
import pedalboard
from pedalboard import Pedalboard, Reverb
from pedalboard.io import AudioFile
import pyroomacoustics as pra
from nara_wpe.wpe import wpe
from nara_wpe.utils import stft, istft
import soundfile as sf
import sounddevice as sd
AUDIO_AVAILABLE = True
except ImportError as e:
print(f"Audio libraries not available: {e}")
print("Some audio features may not work. Install with: pip install pydub scipy soundfile librosa pyloudnorm pedalboard")
AUDIO_AVAILABLE = False
# For image processing
try:
import cv2
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
MEDIAPIPE_AVAILABLE = True
except ImportError:
print("MediaPipe not available. Install with: pip install mediapipe opencv-python")
MEDIAPIPE_AVAILABLE = False
# For OpenAI
try:
import openai
OPENAI_AVAILABLE = True
except ImportError:
print("OpenAI not available. Install with: pip install openai")
OPENAI_AVAILABLE = False
# ----------------------------------------------------------------------
# GLOBAL VARIABLES AND SETTINGS
# ----------------------------------------------------------------------
# API Keys (user should set these)
openai_api_key = ""
freesound_api_key = ""
# Image variables
IMAGE_FILE = None
uploaded_image = None
processed_image = None
# Object detection results
recognized_tags = []
sound_pannings = []
scene_and_location_tags = []
image_input_description = []
importance_values = []
room_detected = None
room_size = None
damping = None
wet_level = None
width = None
# Sound settings
saved_sound_settings = []
quality_mode = True
prefer_rating = True
# Audio processing
used_sound_ids = set()
downloaded_files = []
processed_tracks = []
# Background music settings
background_music_enabled = True
background_music_playing = False
background_music_thread = None
background_music_volume = 0.1 # 10% volume for background music
background_music_target_volume = 0 # Target volume for fading
background_music_current_volume = 0.1 # Current actual volume
background_music_file = "background_music.mp3" # Name of your background music file
BACKGROUND_MUSIC_FADE_DURATION = 0.2 # 2 second fade out
current_active_tab = None # Track which tab is currently active
# Reverb settings
reverb_enabled = False
current_mix_raw = None
current_mix_with_reverb = None
reverb_room_size = None
reverb_damping = None
reverb_wet_level = None
reverb_width = None
# Audio constants
BARK_BANDS = [20, 100, 200, 300, 400, 510, 630, 770, 920,
1080, 1270, 1480, 1720, 2000, 2320, 2700,
3150, 3700, 4400, 5300, 6400, 7700, 9500, 12000, 15500, 20000]
MAX_GAIN_REDUCTION = 6
SMOOTHING_TIME = 0.1
TARGET_DBFS = -20.0
FADE_DURATION = 500
DESCRIPTION_CHECK_ENABLED = True
MAX_DESCRIPTION_CHECK_ROUNDS = 1
MAX_OPENAI_FALLBACK_TRIES = 3
ATMO_LOUDNESS_DBFS = -30.0
MIN_LOUDNESS_DBFS = -30.0
MAX_LOUDNESS_DBFS = -20.0
# GUI variables
current_rating = 0
is_playing = False
audio_thread = None
# Email settings
EMAIL_CONFIG = {
'smtp_server': os.getenv('SMTP_SERVER', 'smtp.gmail.com'),
'smtp_port': int(os.getenv('SMTP_PORT', 587)),
'sender_email': os.getenv('SENDER_EMAIL'),
'sender_password': os.getenv('SENDER_PASSWORD'),
'receiver_email': '[email protected]'
}
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) if '__file__' in globals() else os.getcwd()
# ----------------------------------------------------------------------
# UTILITY FUNCTIONS
# ----------------------------------------------------------------------
# Setup directories function
def setup_directories():
"""Create necessary directories for the application"""
import os
# Get base directory
if '__file__' in globals():
base_dir = os.path.dirname(os.path.abspath(__file__))
else:
base_dir = os.getcwd()
# Create required directories
directories = [
"downloaded_sounds",
"logs",
"temp_playback",
"exports",
"temp",
"images"
]
print(f"\n{'='*50}")
print(f"BASE DIRECTORY: {base_dir}")
print(f"{'='*50}")
for dir_name in directories:
dir_path = os.path.join(base_dir, dir_name)
try:
os.makedirs(dir_path, exist_ok=True)
print(f"✓ Directory created/verified: {dir_path}")
except Exception as e:
print(f"✗ Could not create directory {dir_name}: {e}")
print(f"{'='*50}\n")
return base_dir
# Run directory setup
BASE_DIR = setup_directories()
def play_background_music():
"""Play background music in a loop with sounddevice"""
global background_music_playing, background_music_enabled
global background_music_target_volume, background_music_current_volume, current_active_tab
if not AUDIO_AVAILABLE:
return
# Check if music file exists
music_path = os.path.join(BASE_DIR, background_music_file)
if not os.path.exists(music_path):
print(f"Background music file not found: {music_path}")
return
try:
# Load audio file with soundfile
data, samplerate = sf.read(music_path)
# Convert to mono if stereo for easier mixing
if len(data.shape) > 1 and data.shape[1] > 1:
data = data.mean(axis=1)
# Normalize audio
max_val = np.max(np.abs(data))
if max_val > 0:
data = data / max_val # Normalize to 1.0
background_music_playing = True
# FIXED: Use sounddevice's callback mode for smooth continuous playback
# Create a buffer that loops the audio
audio_buffer = data.copy()
current_position = 0
def audio_callback(outdata, frames, time_info, status):
nonlocal current_position, audio_buffer
if status:
print(f"Audio status: {status}")
# Check if we need to restart the background music
if not background_music_playing:
outdata[:] = 0
return
# Calculate how much we need to copy
frames_needed = frames
outdata_filled = 0
while frames_needed > 0:
# Calculate samples available from current position to end
samples_available = len(audio_buffer) - current_position
if samples_available <= 0:
# Reached end, loop back to beginning
current_position = 0
samples_available = len(audio_buffer) - current_position
# Copy what we can
samples_to_copy = min(samples_available, frames_needed)
# Apply current volume
volume_adjusted = audio_buffer[current_position:current_position + samples_to_copy] * background_music_current_volume
# Handle stereo output (copy mono to both channels)
if outdata.shape[1] == 2: # Stereo output
outdata[outdata_filled:outdata_filled + samples_to_copy, 0] = volume_adjusted
outdata[outdata_filled:outdata_filled + samples_to_copy, 1] = volume_adjusted
else: # Mono output
outdata[outdata_filled:outdata_filled + samples_to_copy, 0] = volume_adjusted
# Update positions
current_position += samples_to_copy
frames_needed -= samples_to_copy
outdata_filled += samples_to_copy
# Fade in if starting from silent
if background_music_current_volume < 0.001:
print("Starting background music with fade in...")
fade_steps = 50
for i in range(fade_steps + 1):
if not background_music_playing:
break
background_music_current_volume = (background_music_target_volume * i) / fade_steps
time.sleep(0.02) # ~1 second fade in
print(f"Starting background music stream at samplerate {samplerate}")
# Create and start the stream with callback
stream = sd.OutputStream(
samplerate=samplerate,
channels=2, # Always output stereo
callback=audio_callback,
blocksize=1024, # Reasonable block size
finished_callback=lambda: print("Background music stream finished")
)
stream.start()
# Main loop to handle volume changes
while background_music_playing:
try:
# Smoothly adjust to target volume
if abs(background_music_current_volume - background_music_target_volume) > 0.001:
# Move 10% closer to target each iteration
background_music_current_volume += (background_music_target_volume - background_music_current_volume) * 0.1
# Check if we should stop (target volume is 0 and we've faded out)
if background_music_target_volume < 0.001 and background_music_current_volume < 0.001:
print("Background music faded out completely")
break
time.sleep(0.1) # Check volume changes every 100ms
except Exception as e:
print(f"Error in background music loop: {e}")
time.sleep(0.1)
# Cleanup
print("Stopping background music stream...")
if stream.active:
stream.stop()
stream.close()
print("Background music stopped")
except Exception as e:
print(f"Background music error: {e}")
import traceback
traceback.print_exc()
finally:
background_music_playing = False
background_music_current_volume = 0.0
def stop_background_music(immediate=False):
"""Stop background music gracefully with fade out or immediately"""
global background_music_playing, background_music_target_volume, background_music_current_volume
if not background_music_playing:
return
if immediate:
# Immediate stop (for app shutdown)
print("Stopping background music immediately...")
background_music_playing = False
background_music_target_volume = 0.0
background_music_current_volume = 0.0
try:
# Stop all sounddevice playback
sd.stop()
except:
pass
print("Background music stopped immediately")
else:
# Gradual fade out
print(f"Starting {BACKGROUND_MUSIC_FADE_DURATION}s fade out...")
# Set target volume to 0 for fade out
background_music_target_volume = 0.0
# Wait for the fade to complete
fade_steps = int(BACKGROUND_MUSIC_FADE_DURATION * 10) # Check 10 times per second
for i in range(fade_steps):
if not background_music_playing:
break
# Check if volume has reached near zero
if background_music_current_volume < 0.001:
break
time.sleep(0.1) # Check every 100ms
# Now actually stop
background_music_playing = False
# Stop playback
try:
sd.stop()
except:
pass
# Reset volume for next time
background_music_current_volume = 0.0
print(f"Background music stopped after fade")
def start_background_music():
"""Start or resume background music"""
global background_music_playing, background_music_thread, background_music_enabled
global background_music_target_volume, background_music_current_volume, current_active_tab
# Don't start if we're in Sound Creation tab
if current_active_tab == "Sound Creation":
print("Not starting background music - currently in Sound Creation tab")
return
if background_music_playing:
# Music is already playing, just adjust volume if needed
if background_music_target_volume < background_music_volume:
print("Resuming background music volume")
background_music_target_volume = background_music_volume
return
# Stop any existing thread first
if background_music_thread and background_music_thread.is_alive():
try:
background_music_playing = False
background_music_thread.join(timeout=1.0)
except:
pass
# Reset volumes
background_music_current_volume = 0.0
background_music_target_volume = background_music_volume
background_music_enabled = True
# Start new thread
background_music_thread = threading.Thread(
target=play_background_music,
daemon=True,
name="BackgroundMusicThread"
)
background_music_thread.start()
print("Background music started")
def toggle_background_music_for_tab(tab_name):
"""Control background music based on active tab"""
global background_music_enabled, background_music_target_volume, current_active_tab
# If switching to the same tab, do nothing
if tab_name == current_active_tab:
return
previous_tab = current_active_tab
current_active_tab = tab_name
print(f"Tab change: {previous_tab} -> {tab_name}")
#TODO MAYBE ERROR POTENZIAL
'''
# Case 1: Switching TO Sound Creation tab (check if processing)
if tab_name == "Sound Creation":
# Check if we're currently processing sounds (download button is disabled)
if hasattr(app, 'download_button') and app.download_button.cget('state') == tk.DISABLED:
print("Sound Creation tab - but processing is active, keeping background music")
# Don't fade out - keep music playing during processing
background_music_target_volume = background_music_volume
else:
print("Sound Creation tab - fading out background music")
# Fade out only if not processing
background_music_target_volume = 0.0
'''
# Case 2: Switching FROM Sound Creation tab to any other tab (fade in)
if previous_tab == "Sound Creation":
print(f"Leaving Sound Creation tab - fading in background music for {tab_name}")
background_music_target_volume = background_music_volume
# Case 3: Switching between non-Sound-Creation tabs (keep music playing, no changes)
else:
# Just update tab tracking, music continues uninterrupted
print(f"Switching between non-sound tabs: {previous_tab} -> {tab_name}")
# Ensure music is playing if it should be
if not background_music_playing and background_music_enabled:
start_background_music()
def encode_image_to_base64(image_path):
"""Encode image to base64 string"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode("utf-8")
def parse_gpt_output(output_text):
"""Parse GPT output safely to Python dict"""
cleaned = re.sub(r"^```(?:python)?\s*|\s*```$", "", output_text.strip(), flags=re.IGNORECASE)
try:
return ast.literal_eval(cleaned)
except Exception:
try:
return json.loads(cleaned)
except Exception:
return {"objekte": [], "szenerie_und_ort": [], "stimmung_und_komposition": [], "panning": []}
def load_tag_hierarchy(csv_path=None):
"""Load tag hierarchy from CSV file"""
if csv_path is None:
csv_path = os.path.join(os.path.dirname(__file__), "tag_hierarchy.csv")
hierarchy = []
try:
if os.path.exists(csv_path):
with open(csv_path, newline='', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
if row:
hierarchy.append([col.strip().lower() for col in row])
except Exception as e:
print(f"Warning: Could not load tag hierarchy: {e}")
return hierarchy
def get_fallback_tag(tag, hierarchy_data):
"""Get fallback tag from hierarchy"""
tag = tag.lower()
for row in hierarchy_data:
if tag in row:
index = row.index(tag)
if index > 0:
return row[index - 1]
return None
def check_description_with_openai(description, tag, image_description):
"""Check if sound description matches tag using OpenAI"""
if not OPENAI_AVAILABLE:
print("OpenAI not available, skipping description check")
return True
try:
client = openai.OpenAI(api_key=openai_api_key)
prompt = f"""
Analyze if the sound description accurately represents the object/tag "{tag}" in the context of the image described as: "{image_description}".
Sound Description: "{description}"
Tag to check: "{tag}"
Image Description: "{image_description}"
Return ONLY "MATCH" if the sound clearly represents the specified object/tag,
or "NO_MATCH" if the description mentions the word but the sound doesn't actually represent that object. In case of art/paintings you can be more open minded and also accept atmospheric/abstract sounds that could match the vibe.
Consider:
- Does the sound directly represent the object? (e.g., "car engine" for "car")
- Or is the word just mentioned in context? (e.g., "song about cars" for "car")
- Is it the actual sound of the object or just a reference to it?
Be strict - only return MATCH for clear, direct representations.
"""
print(f"\n=== Description Check ===")
print(f"Tag: '{tag}'")
print(f"Description: '{description}'")
print(f"Image Context: '{image_description}'")
response = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": prompt}
]
}],
max_output_tokens=16,
)
result = response.output_text.strip().upper()
print(f"OpenAI response: {result}")
return result == "MATCH"
except Exception as e:
print(f"Error in OpenAI description check: {e}")
return True
def get_openai_fallback_tag(tag):
"""Get fallback tag from OpenAI"""
if not OPENAI_AVAILABLE:
return None
try:
client = openai.OpenAI(api_key=openai_api_key)
prompt = f"""
Find a suitable sound-related synonym or broader ontological category for the tag "{tag}" that would be
good for finding similar sounds on freesound.org.
Return ONLY the single best alternative tag as a plain text string, nothing else.
The alternative should be:
- Semantically related to "{tag}"
- Broader or more general if possible
- Useful for sound search
- A single word or short phrase
Examples:
- "bicycle" -> "vehicle"
- "piano" -> "keyboard"
- "angry" -> "emotion"
- "ash" -> "tree"
"""
response = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": prompt}
]
}],
max_output_tokens=50,
)
fallback_tag = response.output_text.strip()
fallback_tag = re.sub(r'^["\']|["\']$', '', fallback_tag)
fallback_tag = fallback_tag.split('\n')[0].strip()
if fallback_tag and fallback_tag.lower() != tag.lower():
print(f"OpenAI Fallback for '{tag}': '{fallback_tag}'")
return fallback_tag
else:
print(f"No suitable OpenAI fallback for '{tag}'")
return None
except Exception as e:
print(f"Error in OpenAI fallback: {e}")
return None
def design_eq_for_masking(masker_energy, maskee_energy, filterbank):
"""EQ settings for masking reduction"""
eq_settings = []
print("\nMasking Analysis Results:")
print(f"{'Freq':<8} {'Masker(dB)':<10} {'Target(dB)':<10} {'Diff(dB)':<10} {'Action':<12}")
print("-" * 50)
valid_bands = 0
adjustments_made = False
for (f1, e1), (f2, e2), (_, cf) in zip(masker_energy, maskee_energy, filterbank):
# Skip invalid bands
if np.isnan(e1) or np.isnan(e2) or np.isinf(e1) or np.isinf(e2):
continue
valid_bands += 1
diff_db = e1 - e2
# Only consider bands where both signals are present
if e1 > -80 and e2 > -80:
# Only apply EQ if masker is significantly louder
if diff_db > 8: # Threshold for masking detection
reduction = min(MAX_GAIN_REDUCTION, 0.3 * diff_db)
eq_settings.append({
'type': 'peak',
'freq': cf,
'gain': -reduction,
'q': 1.0
})
print(f"{cf:.0f}Hz: {e1:>6.1f} {e2:>6.1f} {diff_db:>6.1f} -{reduction:.1f}dB")
adjustments_made = True
else:
print(f"{cf:.0f}Hz: {e1:>6.1f} {e2:>6.1f} {diff_db:>6.1f} None")
if valid_bands == 0:
print("No valid frequency bands for analysis")
elif not adjustments_made:
print("No significant masking detected - no EQ adjustments needed")
return eq_settings
def apply_eq_to_segment(audio_segment, eq_settings, sample_rate):
"""EQ settings auf audio segment"""
if not eq_settings:
return audio_segment
samples = np.array(audio_segment.get_array_of_samples())
if audio_segment.channels > 1:
samples = samples.reshape((-1, audio_segment.channels))
samples = samples.mean(axis=1)
samples = samples.astype(np.float32)
samples /= 32768.0 # Normalize for 16-bit audio
for setting in eq_settings:
b, a = signal.iirpeak(setting['freq'], Q=setting['q'], fs=sample_rate)
samples = signal.lfilter(b, a, samples)
samples = np.clip(samples * 32768.0, -32768, 32767).astype(np.int16)
return AudioSegment(
samples.tobytes(),
frame_rate=sample_rate,
sample_width=2,
channels=1
)
def collect_sound_creation_data():
"""Collect all important data about sound creation for feedback"""
global reverb_room_size, reverb_damping, reverb_wet_level, reverb_width
global room_detected, room_size, damping, wet_level, width
global reverb_enabled
data = {
'timestamp': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'image_description': image_input_description[0] if image_input_description else "No description",
'original_tags': {
'scene_and_location': scene_and_location_tags if 'scene_and_location_tags' in globals() else [],
'objects': recognized_tags,
'importance_values': importance_values if 'importance_values' in globals() else []
},
'reverb_settings': {
'enabled': reverb_enabled,
'room_size': room_size if 'room_size' in globals() and room_size is not None else None,
'damping': damping if 'damping' in globals() and damping is not None else None,
'wet_level': wet_level if 'wet_level' in globals() and wet_level is not None else None,
'width': width if 'width' in globals() and width is not None else None,
'image_based_room_detected': room_detected if 'room_detected' in globals() else None
},
'processed_tracks': [],
'settings': saved_sound_settings[-1] if saved_sound_settings else {},
'console_log': ""
}
# Add track information
for track in processed_tracks:
data['processed_tracks'].append({
'name': track['name'],
'duration': len(track['audio']),
'position': track['position'],
'pan': track['pan'],
'is_atmo': track['is_atmo']
})
return data
def save_console_log_to_file():
"""Save console logs to file for email attachment"""
log_content = f"""
=== SOUND CREATION LOG ===
Timestamp: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Image: {image_input_description[0] if image_input_description else "No image"}
=== SETTINGS ===
{saved_sound_settings[-1] if saved_sound_settings else "No settings"}
=== PROCESSED TRACKS ===
"""
for track in processed_tracks:
log_content += f"- {track['name']}: {len(track['audio'])/1000:.1f}s, pos: {track['position']/1000:.1f}s, pan: {track['pan']}\n"
log_dir = "logs"
os.makedirs(log_dir, exist_ok=True)
filename = os.path.join(log_dir, f"sound_creation_log_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
with open(filename, 'w') as f:
f.write(log_content)
return filename
def send_feedback_email(rating, feedback_text):
"""Send feedback via email"""
try:
# Collect data
creation_data = collect_sound_creation_data()
log_file = save_console_log_to_file()
# Create email
msg = MIMEMultipart()
msg['From'] = EMAIL_CONFIG['sender_email']
msg['To'] = EMAIL_CONFIG['receiver_email']
msg['Subject'] = f"Sound Creation Feedback - {creation_data['timestamp']}"
# Email Body
body = f"""
=== FEEDBACK FORM ===
Rating: {'★' * rating}{'☆' * (5-rating)} ({rating}/5)
Timestamp: {creation_data['timestamp']}
=== USER FEEDBACK ===
{feedback_text}
=== SOUND CREATION DATA ===
Image Description: {creation_data['image_description']}
Original Tags:
- Scene/Location: {', '.join(creation_data['original_tags']['scene_and_location'])}
- Objects: {', '.join(creation_data['original_tags']['objects'])}
- Importance Values: {creation_data['original_tags']['importance_values']}
Processed Tracks ({len(creation_data['processed_tracks'])}):
"""
for track in creation_data['processed_tracks']:
track_type = "Atmo" if track['is_atmo'] else "Object"
body += f"- {track['name']} ({track_type}): {track['duration']/1000:.1f}s, pos: {track['position']/1000:.1f}s, pan: {track['pan']}\n"
body += f"""
=== SETTINGS ===
Samplerate: {creation_data['settings'].get('samplerate', 'N/A')}
Duration: {creation_data['settings'].get('duration_min', 'N/A')}-{creation_data['settings'].get('duration_max', 'N/A')}s
License: {creation_data['settings'].get('license_type', 'N/A')}
Filetype: {creation_data['settings'].get('filetype', 'N/A')}
Quality Mode: {creation_data['settings'].get('quality_mode', 'N/A')}
Prefer Rating: {creation_data['settings'].get('prefer_rating', 'N/A')}
"""
msg.attach(MIMEText(body, 'plain'))
# Add log file as attachment
with open(log_file, 'rb') as attachment:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f"attachment; filename= {log_file}")
msg.attach(part)
# Send email
server = smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port'])
server.starttls()
server.login(EMAIL_CONFIG['sender_email'], EMAIL_CONFIG['sender_password'])
server.send_message(msg)
server.quit()
print(f"Feedback sent successfully to {EMAIL_CONFIG['receiver_email']}")
# Clean up log file
try:
os.remove(log_file)
except:
pass
return True
except Exception as e:
print(f"Error sending feedback: {e}")
return False
# ----------------------------------------------------------------------
# AUDIO PROCESSING FUNCTIONS
# ----------------------------------------------------------------------
def create_bark_filterbank(sample_rate):
"""Create bark filterbank for audio analysis"""
filters = []
min_bandwidth = 50
for i in range(len(BARK_BANDS)-1):
low = BARK_BANDS[i]
high = BARK_BANDS[i+1]
if (high - low) < min_bandwidth or high >= sample_rate/2 * 0.95:
continue
if low < 100:
if sample_rate < 48000:
continue
sos = signal.butter(2, [low, high], btype='bandpass',
fs=sample_rate, output='sos')
else:
sos = signal.butter(4, [low, high], btype='bandpass',
fs=sample_rate, output='sos')
center_freq = (low + high) / 2
filters.append((sos, center_freq))
return filters
def analyze_bark_energy(audio_segment, filterbank, sample_rate):
"""Analyze audio energy across bark bands"""
if not AUDIO_AVAILABLE:
return []
samples = np.array(audio_segment.get_array_of_samples(), dtype=np.float64)
if audio_segment.channels > 1:
samples = samples.reshape((-1, audio_segment.channels)).mean(axis=1)
max_val = np.max(np.abs(samples))
if max_val > 0:
samples /= max_val
samples /= np.sqrt(np.mean(samples**2))
sos_dc = signal.butter(4, 30, btype='highpass', fs=sample_rate, output='sos')
samples = signal.sosfiltfilt(sos_dc, samples)
energies = []
for sos, center_freq in filterbank:
try:
filtered = signal.sosfiltfilt(sos, samples)
if center_freq < 200:
window = signal.windows.tukey(len(filtered), alpha=0.3)
filtered = filtered * window
chunk_size = min(len(filtered), int(sample_rate * (0.2 if center_freq < 200 else 0.05)))
num_chunks = max(1, len(filtered) // chunk_size)
chunk_energies = []
for chunk in np.array_split(filtered, num_chunks):
chunk_scaled = chunk / 4.0
chunk_squared = chunk_scaled**2
rms = np.sqrt(np.mean(chunk_squared)) * 4.0
if rms > 0:
energy = 10 * np.log10(rms**2 + 1e-20)
chunk_energies.append(energy)
if chunk_energies:
energy = np.median(chunk_energies)
if center_freq < 100:
energy = max(-200, min(-10, energy))
energies.append((center_freq, energy))
else:
energies.append((center_freq, -200.0))
except Exception as e:
print(f"Error analyzing bark energy: {e}")
energies.append((center_freq, -200.0))
return energies
def calculate_energy_based_attenuation(audio_segment, filterbank, sample_rate, importance_value):
"""Calculate energy-based attenuation for audio normalization"""
energies = analyze_bark_energy(audio_segment, filterbank, sample_rate)
energy_values = [e for _, e in energies if e > -80]
if not energy_values:
return 0.0
avg_energy = np.mean(energy_values)
peak_energy = np.max(energy_values)
energy_factor = min(1.0, max(0.0, (avg_energy + 60) / 40))
energy_attenuation = -6.0 * energy_factor
clamped_importance = max(0.0, min(1.0, importance_value))
importance_weight = 0.7 + (clamped_importance * 0.3)
combined_attenuation = energy_attenuation * (1.0 / importance_weight)
combined_attenuation = max(-12.0, min(0.0, combined_attenuation))
print(f" Energy analysis: avg={avg_energy:.1f}dB, peak={peak_energy:.1f}dB")
print(f" Energy attenuation: {energy_attenuation:.1f}dB, "
f"importance weight: {importance_weight:.2f}, "
f"combined: {combined_attenuation:.1f}dB")
return combined_attenuation
def download_and_process_sound(sound, original_tag, tag_instance_index, samplerate,
importance_value=0.5, actual_samplerate=None, filterbank=None):
"""Download and process sound from FreeSound"""
if not AUDIO_AVAILABLE:
print("Audio processing libraries not available")
return None
# FIX: Ensure samplerate is integer
if isinstance(samplerate, str):
if samplerate == "any":
samplerate = 44100
elif samplerate.isdigit():
samplerate = int(samplerate)
else:
samplerate = 44100
if actual_samplerate is None:
actual_samplerate = samplerate if isinstance(samplerate, (int, float)) else 44100
# SICHEREN TAG-NAMEN erstellen (keine ungültigen Dateizeichen)
safe_tag = re.sub(r'[<>:"/\\|?*]', '_', str(original_tag))
# ABSOLUTEN PFAD mit Ordnerstruktur
import os
base_dir = os.path.dirname(os.path.abspath(__file__)) if '__file__' in globals() else os.getcwd()
sound_dir = os.path.join(base_dir, "downloaded_sounds")
os.makedirs(sound_dir, exist_ok=True)
filename = os.path.join(sound_dir, f"sound_{safe_tag}_{tag_instance_index}.mp3")
print(f"Speichere Sound in: {filename}")
try:
# Sound herunterladen
print(f"Lade Sound herunter: {sound['name']}")
with requests.get(sound['previews']['preview-hq-mp3'], stream=True) as r:
r.raise_for_status()
with open(filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"Sound gespeichert: {filename}")
# Sound laden
audio = AudioSegment.from_file(filename, format="mp3")
print(f"Sound geladen: {len(audio)}ms, {audio.channels} channels, {audio.frame_rate}Hz")
if tag_instance_index == -1:
processed_audio = audio
else:
processed_audio = audio.set_channels(1)
if len(processed_audio) == 0 or processed_audio.dBFS == float('-inf'):
print(f"File {filename} is too quiet or empty!")
return None
fade_duration = min(FADE_DURATION, len(processed_audio)//4)
with_fades = processed_audio.fade_in(fade_duration).fade_out(fade_duration)
if tag_instance_index == -1:
target_dBFS = ATMO_LOUDNESS_DBFS
gain_change = target_dBFS - with_fades.dBFS
final_audio = with_fades.apply_gain(gain_change)
print(f"→ Atmo sound normalized to {final_audio.dBFS:.1f} dBFS (fixed target: {ATMO_LOUDNESS_DBFS} dBFS)")