SephStanek commited on
Commit
0e7ae00
·
verified ·
1 Parent(s): cc9dfd0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -296
app.py CHANGED
@@ -1,316 +1,221 @@
1
- import gradio as gr
2
- import librosa
3
- import numpy as np
4
- import matplotlib.pyplot as plt
5
- import io
6
- import reportlab
7
- from reportlab.pdfgen import canvas
8
- from reportlab.lib.pagesizes import letter
9
- from reportlab.lib.units import inch
10
- from reportlab.lib import colors
11
  from reportlab.pdfbase import pdfmetrics
12
  from reportlab.pdfbase.ttfonts import TTFont
 
 
 
13
  from reportlab.lib.utils import ImageReader
14
- import qrcode
15
- import tempfile
16
- import datetime
17
- import os
18
-
19
- # ===============================
20
- # REGISTER PLAYFAIR DISPLAY BOLD
21
- # ===============================
22
- pdfmetrics.registerFont(TTFont("PlayfairBold", "PlayfairDisplay-Bold.ttf"))
23
-
24
- # ============================================
25
- # LOAD LOGO FROM ROOT (logo.png)
26
- # ============================================
27
- LOGO_PATH = "logo.png"
28
-
29
- # ================================
30
- # COLOR PALETTE (LUXE MODE)
31
- # ================================
32
- AUBERGINE = colors.HexColor("#3B0B3F")
33
- ACCENT = colors.HexColor("#8B5CF6")
34
- GREEN = colors.HexColor("#1FA749")
35
- YELLOW = colors.HexColor("#F6C434")
36
- RED = colors.HexColor("#D83131")
37
-
38
- # ================================
39
- # UTILITY FUNCTIONS
40
- # ================================
41
-
42
- def detect_bpm(y, sr):
43
- try:
44
- tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
45
- return round(float(tempo))
46
- except:
47
- return None
48
 
49
- def detect_key(y, sr):
50
- try:
51
- chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
52
- chroma_mean = np.mean(chroma, axis=1)
53
- key_index = np.argmax(chroma_mean)
54
- KEYS = ["C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B"]
55
- return KEYS[key_index]
56
- except:
57
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- def detect_voice_range(pitches):
60
- valid = pitches[pitches > 0]
61
- if len(valid) == 0:
62
- return "Unknown range"
63
 
64
- median_pitch = np.median(valid)
65
- if median_pitch < 150: return "Bass / Baritone Piece"
66
- if median_pitch < 250: return "Tenor / Alto Piece"
67
- if median_pitch < 350: return "Mezzo / Soprano Piece"
68
- return "High Soprano / Extreme Range Piece"
69
 
70
- def detect_midi_like(y):
71
  try:
72
- onset_env = librosa.onset.onset_strength(y=y)
73
- diffs = np.diff(onset_env)
74
- jitter = np.std(diffs)
75
- return jitter < 0.01
76
- except:
77
- return False
78
-
79
- def shade_score(ai_like):
80
- # direct mapping
81
- return round(float(ai_like) * 1.0, 1)
82
-
83
- def metric_color(value, metric_type):
84
- """Returns Green/Yellow/Red depending on thresholds."""
85
- if metric_type == "ai":
86
- if value <= 25: return GREEN
87
- if value <= 75: return YELLOW
88
- return RED
89
- if metric_type == "human":
90
- if value >= 75: return GREEN
91
- if value >= 25: return YELLOW
92
- return RED
93
- if metric_type == "auto":
94
- # always orange block
95
- return colors.HexColor("#F59E0B")
96
- return AUBERGINE
97
-
98
- # ======================================
99
- # PDF GENERATOR (LUXE VERSION)
100
- # ======================================
101
- def generate_pdf(filename, ai_like, human_like, auto_index, shade, style, tone,
102
- key, bpm, voice_range, is_midi, transcript):
103
-
104
- out = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
105
- c = canvas.Canvas(out.name, pagesize=letter)
106
- width, height = letter
107
-
108
- # ======================================
109
- # HEADER
110
- # ======================================
111
- c.setFont("PlayfairBold", 24)
112
- c.setFillColor(AUBERGINE)
113
- c.drawString(50, height - 70, "Tour de Fierce™")
114
-
115
- c.setFont("PlayfairBold", 16)
116
- c.drawString(50, height - 95, "Audio Clapback Report™")
117
-
118
- # timestamp
119
- c.setFont("Helvetica", 10)
120
- c.setFillColor(colors.black)
121
- c.drawString(50, height - 112, f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
122
-
123
- # filename
124
- c.setFont("Helvetica-Bold", 11)
125
- c.drawString(50, height - 130, f"Report for: {filename}")
126
-
127
- # LOGO (top left)
128
- try:
129
- logo = ImageReader(LOGO_PATH)
130
- c.drawImage(logo, width - 150, height - 120, width=100, preserveAspectRatio=True, mask='auto')
131
  except:
132
  pass
133
 
134
- # QR CODE (top right)
135
- qr = qrcode.make("https://www.tourdefierce.vip/audio-clapback-engine-ai-detector-autotune-detector")
136
- qr_buffer = io.BytesIO()
137
- qr.save(qr_buffer)
138
- qr_buffer.seek(0)
139
- c.drawImage(ImageReader(qr_buffer), width - 150, height - 250, width=90, height=90)
140
-
141
- # line
142
- c.setStrokeColor(colors.lightgrey)
143
  c.setLineWidth(1)
144
- c.line(40, height - 260, width - 40, height - 260)
145
-
146
- # ======================================
147
- # METRIC BOXES
148
- # ======================================
149
-
150
- def draw_metric(label, value, x, color):
151
- c.setFillColor(color)
152
- c.rect(x, height - 340, 160, 60, fill=1, stroke=0)
153
- c.setFillColor(colors.white)
154
- c.setFont("Helvetica-Bold", 12)
155
- c.drawString(x + 10, height - 315, label)
156
- c.setFont("Helvetica-Bold", 20)
157
- c.drawString(x + 10, height - 335, f"{value}%")
158
-
159
- draw_metric("AI Likelihood", ai_like, 50, metric_color(ai_like, "ai"))
160
- draw_metric("Human Likelihood", human_like, 240, metric_color(human_like, "human"))
161
-
162
- # autotune block
163
- c.setFillColor(metric_color(auto_index, "auto"))
164
- c.rect(430, height - 340, 160, 60, fill=1, stroke=0)
165
- c.setFillColor(colors.white)
166
- c.setFont("Helvetica-Bold", 12)
167
- c.drawString(440, height - 315, "Autotune Index")
168
- c.setFont("Helvetica-Bold", 20)
169
- c.drawString(440, height - 335, f"{auto_index}/100")
170
-
171
- # ======================================
172
- # SHADE METER (OWN SECTION)
173
- # ======================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  c.setFillColor(ACCENT)
175
- c.roundRect(50, height - 430, 540, 60, 8, fill=0, stroke=1)
176
-
177
- c.setFont("Helvetica-Bold", 12)
178
- c.setFillColor(AUBERGINE)
179
- c.drawString(65, height - 385, f"Shade Meter: {shade}/100")
180
 
 
181
  c.setFont("Helvetica", 10)
182
- c.setFillColor(colors.black)
183
- c.drawString(65, height - 400,
184
- "The Shade Meter reflects the clip’s transparency score: higher = shadier.")
185
-
186
- verdict = ""
187
- if shade < 20: verdict = "You avoided the shade parade — clean & clear!"
188
- elif shade < 50: verdict = "A lil’ shady lady moment — but nothing wild."
189
- else: verdict = "Oop! You got sprayed with shade — this clip is looking sus."
190
-
191
- c.setFont("Helvetica-Oblique", 10)
192
- c.drawString(65, height - 415, verdict)
193
-
194
- # ======================================
195
- # BREAKDOWN SECTION
196
- # ======================================
197
- cursor = height - 460
198
- c.setFont("PlayfairBold", 16)
199
- c.setFillColor(AUBERGINE)
200
- c.drawString(50, cursor, "Full Forensic Breakdown:")
201
- cursor -= 25
202
-
203
- text = c.beginText(50, cursor)
204
- text.setFont("Helvetica", 10)
205
- text.setLeading(14)
206
-
207
- multi_voice_note = ""
208
- if "multi" in voice_range.lower():
209
- multi_voice_note = "Multiple voices detected — metrics averaged."
210
-
211
- midi_note = "Instrument timing is too perfect — likely MIDI or programmed." if is_midi else \
212
- "Timing variance suggests human-played instrumentation."
213
-
214
- transcript_note = transcript if transcript else "(No transcript available)"
215
-
216
- narrative = f"""
217
- AI Likelihood: {ai_like}%. Human Likelihood: {human_like}%.
218
- Autotune Index: {auto_index}/100. Shade Meter: {shade}/100.
219
- Detected Vocal Style: {style}. Clapback Tone: {tone}.
220
 
221
- Key Analysis: {key if key else "Unknown"}.
222
- Estimated BPM: {bpm if bpm else "N/A"}.
223
- Piece Voice Range Classification: {voice_range}. {multi_voice_note}
224
-
225
- Timing & Instrumentation: {midi_note}
226
-
227
- Transcript Sample:
228
- "{transcript_note}"
229
-
230
- Interpretation:
231
- This forensic-style analysis blends embeddings, pitch stability, and spectral norms to judge how
232
- organic or synthetic the performance may be. Lower jitter & shimmer often indicate machine-made
233
- precision, whereas organic vocals tend to include human irregularities. The Shade Meter synthesizes
234
- all metrics into a transparency index. Treat this as expert guidance, not legal certification.
235
- """
236
-
237
- for line in narrative.split("\n"):
238
- text.textLine(line.strip())
239
-
240
- c.drawText(text)
241
-
242
- # footer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  c.setFont("Helvetica", 8)
244
- c.setFillColor(colors.grey)
245
- c.drawString(50, 30, "© 2025 Tour de Fierce™ — All Shade. No Shame. www.tourdefierce.vip")
 
246
 
247
- c.showPage()
248
  c.save()
 
249
 
250
- return out.name
251
-
252
- # ======================================
253
- # MAIN ANALYSIS FUNCTION
254
- # ======================================
255
- def analyze(audio_file, tone):
256
- if audio_file is None:
257
- return "Upload an audio file", None
258
-
259
- filename = os.path.basename(audio_file)
260
-
261
- y, sr = librosa.load(audio_file, sr=44100)
262
- pitches, _ = librosa.piptrack(y=y, sr=sr)
263
- detected_pitches = pitches[pitches > 0]
264
-
265
- # fake ai/human for now
266
- ai_like = np.random.uniform(5, 95)
267
- human_like = 100 - ai_like
268
- auto_index = round(np.random.uniform(0, 60), 1)
269
-
270
- shade = shade_score(ai_like)
271
- voice_range = detect_voice_range(detected_pitches)
272
- key = detect_key(y, sr)
273
- bpm = detect_bpm(y, sr)
274
- is_midi = detect_midi_like(y)
275
-
276
- transcript = "Transcript unavailable in preview."
277
-
278
- pdf = generate_pdf(filename, round(ai_like), round(human_like),
279
- auto_index, shade, "Auto Style", tone,
280
- key, bpm, voice_range, is_midi, transcript)
281
-
282
- summary = f"""
283
- AI: {round(ai_like)}%
284
- Human: {round(human_like)}%
285
- Autotune: {auto_index}/100
286
- Shade: {shade}/100
287
- Key: {key}
288
- BPM: {bpm}
289
- Voice Range: {voice_range}
290
- """
291
-
292
- return summary, pdf
293
-
294
- # ======================================
295
- # UI
296
- # ======================================
297
-
298
- with gr.Blocks(css="""
299
- #title { font-family: 'Playfair Display', serif; font-weight: 700; font-size: 38px; color: #3B0B3F; }
300
- """) as iface:
301
-
302
- gr.HTML("<h1 id='title'>👏 Tour de Fierce Audio Clapback Engine™</h1>")
303
- gr.HTML("<p style='color:white;'>Detects AI Vocals • Autotune • Shade Levels • Audio Forensic Reports</p>")
304
-
305
- audio = gr.Audio(type="filepath", label="Upload Audio File (Max 45s)")
306
-
307
- tone = gr.Radio(["professional", "sassy", "forensic"], value="professional", label="Clapback Tone")
308
-
309
- summary = gr.Textbox(label="Clapback Summary")
310
- pdf_output = gr.File(label="Full PDF Report")
311
-
312
- run = gr.Button("Run Clapback 👏", variant="primary")
313
-
314
- run.click(analyze, inputs=[audio, tone], outputs=[summary, pdf_output])
315
-
316
- iface.launch()
 
1
+ # ----------------------------------------
2
+ # PDF GENERATION – LUXE EDITION
3
+ # ----------------------------------------
4
+
 
 
 
 
 
 
5
  from reportlab.pdfbase import pdfmetrics
6
  from reportlab.pdfbase.ttfonts import TTFont
7
+ from reportlab.lib.pagesizes import letter
8
+ from reportlab.lib import colors
9
+ from reportlab.pdfgen import canvas
10
  from reportlab.lib.utils import ImageReader
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ # Register Playfair Display Bold (must match file name EXACTLY)
13
+ pdfmetrics.registerFont(TTFont("PlayfairDisplay-Bold", "PlayfairDisplay-Bold.ttf"))
14
+ pdfmetrics.registerFont(TTFont("Helvetica", "Helvetica"))
15
+
16
+ ACCENT = colors.HexColor("#8b5cf6") # r8b5cf6
17
+ PRIMARY = colors.HexColor("#3b0c3f") # eggplant/aubergine
18
+ LIGHT_GRAY = colors.HexColor("#e6e6e6")
19
+ BLACK = colors.black
20
+ WHITE = colors.white
21
+
22
+ ENGINE_URL = "https://www.tourdefierce.vip/audio-clapback-engine-ai-detector-autotune-detector"
23
+
24
+
25
+ def make_pdf(
26
+ logo_path,
27
+ ai_score,
28
+ human_score,
29
+ atune,
30
+ shade,
31
+ style,
32
+ tone,
33
+ transcript,
34
+ clip_title
35
+ ):
36
+ buffer = io.BytesIO()
37
+ c = canvas.Canvas(buffer, pagesize=letter)
38
+
39
+ W, H = letter
40
+
41
+ # -------------------------------
42
+ # HEADER AREA
43
+ # -------------------------------
44
+ c.setFillColor(WHITE)
45
+ c.rect(0, 0, W, H, fill=1)
46
+
47
+ # Logo
48
+ c.drawImage("logo.png", 40, H - 120, width=90, height=90, preserveAspectRatio=True)
49
+
50
+ # Brand name
51
+ c.setFillColor(PRIMARY)
52
+ c.setFont("PlayfairDisplay-Bold", 32)
53
+ c.drawString(150, H - 60, "Tour de Fierce")
54
+
55
+ # Subtitle
56
+ c.setFillColor(ACCENT)
57
+ c.setFont("Helvetica", 14)
58
+ c.drawString(150, H - 82, "Audio Clapback Report™")
59
 
60
+ # Timestamp
61
+ c.setFillColor(BLACK)
62
+ c.setFont("Helvetica", 9)
63
+ c.drawString(150, H - 98, f"Generated: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}")
64
 
65
+ # Clip Title
66
+ c.setFont("Helvetica", 11)
67
+ c.drawString(40, H - 145, f"Clip Analyzed: {clip_title}")
 
 
68
 
69
+ # QR Code (top right)
70
  try:
71
+ import qrcode
72
+ qr = qrcode.make(ENGINE_URL)
73
+ qr_buffer = io.BytesIO()
74
+ qr.save(qr_buffer, format="PNG")
75
+ qr_buffer.seek(0)
76
+ qr_img = ImageReader(qr_buffer)
77
+ c.drawImage(qr_img, W - 120, H - 140, width=80, height=80)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  except:
79
  pass
80
 
81
+ # Divider line
82
+ c.setStrokeColor(LIGHT_GRAY)
 
 
 
 
 
 
 
83
  c.setLineWidth(1)
84
+ c.line(40, H - 160, W - 40, H - 160)
85
+
86
+ # -------------------------------
87
+ # SCORE BOXES (AI / HUMAN / AUTOTUNE)
88
+ # -------------------------------
89
+
90
+ def score_color(score, invert=False):
91
+ """
92
+ invert=False green=human good / red=AI high
93
+ invert=True → green=AI low / red=AI high
94
+ """
95
+ if not invert:
96
+ # human
97
+ if score >= 75:
98
+ return colors.green
99
+ if score >= 25:
100
+ return colors.yellow
101
+ return colors.red
102
+ else:
103
+ # AI
104
+ if score <= 25:
105
+ return colors.green
106
+ if score <= 75:
107
+ return colors.yellow
108
+ return colors.red
109
+
110
+ # AI %
111
+ c.setFillColor(score_color(ai_score, invert=True))
112
+ c.rect(40, H - 250, 150, 80, fill=1)
113
+ c.setFillColor(WHITE)
114
+ c.setFont("Helvetica", 11)
115
+ c.drawString(55, H - 190, "AI Likelihood")
116
+ c.setFont("PlayfairDisplay-Bold", 28)
117
+ c.drawString(55, H - 215, f"{ai_score:.1f}%")
118
+
119
+ # HUMAN %
120
+ c.setFillColor(score_color(human_score))
121
+ c.rect(210, H - 250, 150, 80, fill=1)
122
+ c.setFillColor(WHITE)
123
+ c.setFont("Helvetica", 11)
124
+ c.drawString(225, H - 190, "Human Likelihood")
125
+ c.setFont("PlayfairDisplay-Bold", 28)
126
+ c.drawString(225, H - 215, f"{human_score:.1f}%")
127
+
128
+ # AUTOTUNE
129
+ c.setFillColor(colors.orange)
130
+ c.rect(380, H - 250, 150, 80, fill=1)
131
+ c.setFillColor(WHITE)
132
+ c.setFont("Helvetica", 11)
133
+ c.drawString(395, H - 190, "Autotune Index")
134
+ c.setFont("PlayfairDisplay-Bold", 28)
135
+ c.drawString(395, H - 215, f"{atune:.1f}/100")
136
+
137
+ # -------------------------------
138
+ # SHADE METER (Luxury Bar)
139
+ # -------------------------------
140
+ c.setFont("Helvetica", 11)
141
+ c.setFillColor(BLACK)
142
+ c.drawString(40, H - 280, "Shade Meter")
143
+ c.setFont("Helvetica", 9)
144
+ c.drawString(40, H - 292, "0 = no shade, 100 = maximum clapback")
145
+
146
+ # meter background
147
+ c.setFillColor(LIGHT_GRAY)
148
+ c.rect(40, H - 305, 490, 12, fill=1, stroke=0)
149
+
150
+ # meter fill
151
  c.setFillColor(ACCENT)
152
+ fill_width = (shade / 100) * 490
153
+ c.rect(40, H - 305, fill_width, 12, fill=1, stroke=0)
 
 
 
154
 
155
+ c.setFillColor(BLACK)
156
  c.setFont("Helvetica", 10)
157
+ c.drawString(540, H - 304, f"{shade:.1f} / 100")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ # Shade text
160
+ c.setFont("Helvetica", 10)
161
+ c.setFillColor(BLACK)
162
+ if shade < 10:
163
+ shade_text = "You're practically a saint — no shade detected!"
164
+ elif shade < 40:
165
+ shade_text = "A lil’ sprinkle of shade, but nothing major."
166
+ elif shade < 75:
167
+ shade_text = "Okay shady lady! There are *elements* to discuss."
168
+ else:
169
+ shade_text = "Girl… you were sprayed with shade. Maximum clapback."
170
+
171
+ c.drawString(40, H - 325, shade_text)
172
+
173
+ # -------------------------------
174
+ # ANALYSIS SUMMARY
175
+ # -------------------------------
176
+
177
+ c.setFont("PlayfairDisplay-Bold", 18)
178
+ c.setFillColor(PRIMARY)
179
+ c.drawString(40, H - 360, "Analysis Summary")
180
+
181
+ c.setFont("Helvetica", 11)
182
+ c.setFillColor(BLACK)
183
+ c.drawString(40, H - 385, f"Detected Vocal Style: {style}")
184
+ c.drawString(40, H - 402, f"Clapback Style: {tone}")
185
+ c.drawString(40, H - 419, f"Shade Meter: {shade:.1f}/100")
186
+
187
+ # -------------------------------
188
+ # FORENSIC BREAKDOWN
189
+ # -------------------------------
190
+
191
+ c.setFont("PlayfairDisplay-Bold", 18)
192
+ c.setFillColor(PRIMARY)
193
+ c.drawString(40, H - 455, "Full Forensic Breakdown:")
194
+
195
+ text_y = H - 480
196
+ c.setFont("Helvetica", 10)
197
+ c.setFillColor(BLACK)
198
+
199
+ for line in transcript.replace("\n", " ").split(". "):
200
+ c.drawString(40, text_y, line.strip() + ".")
201
+ text_y -= 14
202
+ if text_y < 60:
203
+ c.showPage()
204
+ text_y = H - 60
205
+ c.setFont("Helvetica", 10)
206
+
207
+ # Footer
208
+ c.setStrokeColor(LIGHT_GRAY)
209
+ c.line(40, 50, W - 40, 50)
210
  c.setFont("Helvetica", 8)
211
+ c.setFillColor(BLACK)
212
+ c.drawString(40, 37, "© 2025 Tour de Fierce™ — All Shade, No Shame.")
213
+ c.drawString(300, 37, "www.tourdefierce.vip")
214
 
 
215
  c.save()
216
+ buffer.seek(0)
217
 
218
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
219
+ tmp.write(buffer.getvalue())
220
+ tmp.close()
221
+ return tmp.name