NOBODY204 commited on
Commit
e6b8a32
Β·
verified Β·
1 Parent(s): ff288b7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +334 -199
app.py CHANGED
@@ -1,211 +1,346 @@
1
  # -*- coding: utf-8 -*-
2
  """
3
- Antigravity Shield v5.0 β€” ACoNum / Trusted Sound 2026
 
4
  =====================================================
5
- Fusion : AASIST neural anti-spoofing + Jitter/Flatness + AcoustID
6
- RΓ©sout :
7
- - Faux positifs sur voix off (is.mp3)
8
- - Anciennes chansons (vintage/radio)
9
- - AI Cover complexes (ElevenLabs, RVC)
10
- - Broadcast FM (jitter Γ©levΓ© naturel)
 
 
11
  """
12
 
13
- import numpy as np
14
- import librosa
15
- import librosa.display
16
- import matplotlib.pyplot as plt
17
- import noisereduce as nr
18
- import hashlib
19
  import os
20
  import json
21
  import subprocess
22
- import gradio as gr
23
-
24
- # ══════════════════════════════════════════════════════════════
25
- # CONFIG
26
- # ══════════════════════════════════════════════════════════════
27
-
28
- ACOUSTID_KEY = "TY6HUQsigs"
29
- AASIST_MODEL_ID = "Mahmoud-Yassen/aasist-antispoof"
30
-
31
- # Seuils calibrΓ©s pour ACoNum 2026
32
- JITTER_BROADCAST_MIN = 0.85
33
- FLATNESS_THRESHOLD = 0.0012
34
- FLATNESS_BROADCAST = 0.0010
35
- CENTROID_BROADCAST = 3500.0
36
- CENTROID_VINTAGE = 3000.0
37
- ZCR_SPEECH_THRESHOLD = 0.055 # DΓ©tection voix off/parlΓ©e
38
- ZCR_VINTAGE = 0.06
39
-
40
- class AntigravityShield:
41
- def __init__(self):
42
- print("πŸš€ Antigravity Shield v5.0 β€” AASIST + Jitter + AcoustID")
43
- self.ast_model = None
44
- self.aasist = None
45
- self.aasist_ok = False
46
-
47
- def load_ast(self):
48
- if self.ast_model is None:
49
- try:
50
- from transformers import pipeline
51
- self.ast_model = pipeline("audio-classification", model="MIT/ast-finetuned-audioset-10-10-0.4593")
52
- print("βœ… AST chargΓ©")
53
- except Exception as e:
54
- print(f"⚠️ AST non disponible : {e}")
55
-
56
- def load_aasist(self):
57
- if self.aasist is None:
58
- try:
59
- from transformers import pipeline
60
- candidates = ["Mahmoud-Yassen/aasist-antispoof", "m-aliabbas/AASIST"]
61
- for model_id in candidates:
62
- try:
63
- self.aasist = pipeline("audio-classification", model=model_id, sampling_rate=16000)
64
- self.aasist_ok = True
65
- print(f"βœ… AASIST chargΓ© : {model_id}")
66
- break
67
- except: continue
68
- except Exception as e:
69
- print(f"⚠️ AASIST erreur : {e}")
70
-
71
- def get_sha256(self, path):
72
- with open(path, "rb") as f:
73
- return hashlib.sha256(f.read()).hexdigest()
74
-
75
- def acoustid_lookup(self, path):
76
- try:
77
- import acoustid
78
- results = acoustid.match(ACOUSTID_KEY, path)
79
- for score, recording_id, title, artist in results:
80
- if score > 0.8: return True, f"{artist} β€” {title}", recording_id
81
- return False, "Non identifiΓ©", None
82
- except:
83
- return None, "AcoustID non dispo", None
84
-
85
- def generer_spectrogramme(self, y, sr, jitter_a, jitter_b, aasist_score):
86
- fig, axes = plt.subplots(1, 2, figsize=(14, 4))
87
- S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)
88
- S_dB = librosa.power_to_db(S, ref=np.max)
89
- librosa.display.specshow(S_dB, sr=sr, x_axis='time', y_axis='mel', cmap='magma', ax=axes[0])
90
- axes[0].set_title('Texture Vocale (Mel)')
91
-
92
- # Timeline simplifiΓ©e
93
- axes[1].axhline(y=jitter_b, color='yellow', linestyle='--', label=f'Jitter-B: {jitter_b:.3f}')
94
- if aasist_score: axes[1].axhline(y=aasist_score, color='red', linestyle=':', label=f'AASIST: {aasist_score:.2f}')
95
- axes[1].set_title('MΓ©triques de StabilitΓ©')
96
- axes[1].legend()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- plt.tight_layout()
99
- plot_path = "spectrum_v5.png"
100
- plt.savefig(plot_path, dpi=120)
101
- plt.close()
102
- return plot_path
103
-
104
- def run_aasist(self, path):
105
- if not self.aasist_ok: return None, "AASIST Off"
106
- try:
107
- results = self.aasist(path)
108
- for r in results:
109
- if any(k in r['label'].lower() for k in ['spoof', 'fake', 'synthetic']):
110
- return float(r['score']), f"AASIST spoof={r['score']:.1%}"
111
- return 1 - float(results[0]['score']), "AASIST Bonafide"
112
- except: return None, "AASIST Error"
113
-
114
- def analyser_expert(self, path):
115
- if path is None: return None, "En attente...", None
116
- try:
117
- self.load_ast()
118
- self.load_aasist()
119
- y, sr = librosa.load(path, sr=44100)
120
-
121
- # Analyse des caractΓ©ristiques
122
- zcr = float(np.mean(librosa.feature.zero_crossing_rate(y)))
123
- centroid = float(np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)))
124
- flatness = float(np.mean(librosa.feature.spectral_flatness(y=y)))
125
-
126
- # --- DÉTECTION VOIX OFF / SPEECH (Correction is.mp3) ---
127
- is_speech = zcr < ZCR_SPEECH_THRESHOLD
128
- is_vintage = centroid < CENTROID_VINTAGE and zcr < ZCR_VINTAGE
129
-
130
- # DΓ©bruitage adaptatif
131
- prop = 0.4 if (is_speech or is_vintage) else 0.7
132
- y_denoised = nr.reduce_noise(y=y, sr=sr, prop_decrease=prop)
133
-
134
- def compute_jitter(sig):
135
- pitches, mags = librosa.piptrack(y=sig, sr=sr, fmin=60, fmax=4000)
136
- mask = mags > np.median(mags)
137
- return float(np.std(pitches[mask]) / 1000) if np.any(mask) else 0.0
138
-
139
- jitter_a = compute_jitter(y)
140
- jitter_b = compute_jitter(y_denoised)
141
- aasist_score, aasist_label = self.run_aasist(path)
142
- acoustid_known, acoustid_info, _ = self.acoustid_lookup(path)
143
-
144
- # --- LOGIQUE DE SCORING FUSIONNÉE ---
145
- confiance = 50
146
- raisons = []
147
-
148
- # Correction Voix Off / Vintage
149
- if is_speech:
150
- confiance += 20
151
- raisons.append("πŸ—£οΈ DΓ©tection Voix Off : Seuil de tolΓ©rance augmentΓ©")
152
- if is_vintage:
153
- confiance += 15
154
- raisons.append("πŸ•°οΈ Signature Vintage/Radio dΓ©tectΓ©e")
155
-
156
- # AASIST
157
- if aasist_score and aasist_score > 0.75:
158
- if not is_speech: # Moins punitif sur la voix off
159
- confiance -= 40
160
- raisons.append("πŸ€– AASIST dΓ©tecte une synthΓ¨se neuronale")
161
- else:
162
- confiance -= 15
163
- raisons.append("⚠️ AASIST suspect, mais contexte Voix Off")
164
-
165
- # Texture
166
- if flatness > FLATNESS_THRESHOLD:
167
- confiance += 15
168
- raisons.append(f"🌿 Texture organique (Flatness OK)")
169
- else:
170
- if not is_speech:
171
- confiance -= 20
172
- raisons.append("πŸ”‡ Signal trop lisse (CaractΓ©ristique IA)")
173
-
174
- # Verdict final
175
- confiance = max(0, min(100, confiance))
176
- if confiance >= 70: verdict = "πŸ”’ AUTHENTIQUE CERTIFIΓ‰"
177
- elif confiance >= 40: verdict = "⚠️ ANALYSE INCONCLUSIVE"
178
- else: verdict = "πŸ›‘ AI COVER / DEEPFAKE DΓ‰TECTΓ‰"
179
-
180
- report = f"πŸ“Œ RAPPORT v5.0\n🎯 CONFIANCE : {confiance}%\n>>> VERDICT : {verdict}\n\nNOTES :\n" + "\n".join([f" β€’ {r}" for r in raisons])
181
-
182
- return (
183
- self.generer_spectrogramme(y, sr, jitter_a, jitter_b, aasist_score),
184
- report,
185
- { "Score": f"{confiance}%", "Vintage": is_vintage, "VoixOff": is_speech, "Flatness": round(flatness, 6) }
186
- )
187
- except Exception as e:
188
- return None, f"Erreur : {e}", None
189
-
190
- # ══════════════════════════════════════════════════════════════
191
- # INTERFACE
192
- # ══════════════════════════════════════════════════════════════
193
- shield = AntigravityShield()
194
- with gr.Blocks(theme=gr.themes.Soft()) as demo:
195
- gr.Markdown("# πŸ›‘οΈ Antigravity Shield v5.0 PRO")
196
- with gr.Row():
197
- with gr.Column():
198
- audio_input = gr.Audio(type="filepath", label="Fichier Audio")
199
- run_btn = gr.Button("βš™οΈ ANALYSER", variant="primary")
200
- with gr.Column():
201
- image_output = gr.Image(label="Spectrogramme")
202
- report_output = gr.Textbox(label="Rapport d'expertise", lines=12)
203
- metrics_output = gr.JSON(label="MΓ©triques")
204
-
205
- run_btn.click(fn=shield.analyser_expert, inputs=audio_input, outputs=[image_output, report_output, metrics_output])
206
-
207
- if __name__ == "__main__":
208
- demo.launch(server_name="0.0.0.0", server_port=7860)
209
 
210
 
211
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ analyze_media.py β€” Pont Python AI pour MAM SHIELD v5.2
4
+ ACoNum / Trusted Sound 2026
5
  =====================================================
6
+ Usage (appelΓ© par server.js) :
7
+ python analyze_media.py audio <chemin_fichier>
8
+ python analyze_media.py video <chemin_fichier>
9
+ python analyze_media.py image <chemin_fichier>
10
+ python analyze_media.py pdf <chemin_fichier>
11
+
12
+ Retourne un JSON sur stdout :
13
+ { "summary": "...", "tags": [...], "transcript": "...", "flac_ok": true, "flac_path": "..." }
14
  """
15
 
16
+ import sys
 
 
 
 
 
17
  import os
18
  import json
19
  import subprocess
20
+ import traceback
21
+
22
+ # ── Config Ollama ────────────────────────────────────────────
23
+ OLLAMA_HOST = 'http://localhost:11434'
24
+ OLLAMA_MODEL = 'qwen2.5:3b' # tags + rΓ©sumΓ© standard
25
+
26
+ # ── Nemotron β€” auto-dΓ©tection au dΓ©marrage ───────────────
27
+ # Modèle téléchargé : nvidia/Nemotron-3-Nano-Omni-30B → version 4B locale
28
+ # Nom Ollama attendu : nemotron-mini ou nemotron-mini:4b
29
+ NEMOTRON_MODEL = None
30
+ NEMOTRON_PRIORITY = [ # ordre de prioritΓ© exact
31
+ 'nemotron-mini:4b',
32
+ 'nemotron-mini:latest',
33
+ 'nemotron-mini',
34
+ 'nemotron:4b',
35
+ 'nemotron:mini',
36
+ 'nemotron:latest',
37
+ 'nvidia/nemotron-mini',
38
+ 'nemotron-nano',
39
+ 'nemotron',
40
+ ]
41
+
42
+ def detect_nemotron():
43
+ """Cherche automatiquement un modèle Nemotron dans Ollama (priorité 4B mini)."""
44
+ global NEMOTRON_MODEL
45
+ try:
46
+ import urllib.request as ur
47
+ req = ur.Request(OLLAMA_HOST + '/api/tags', method='GET')
48
+ with ur.urlopen(req, timeout=5) as r:
49
+ data = json.loads(r.read().decode('utf-8'))
50
+ models = [m.get('name','') for m in data.get('models', [])]
51
+ # Cherche par prioritΓ© exacte d'abord
52
+ for prio in NEMOTRON_PRIORITY:
53
+ if prio in models:
54
+ NEMOTRON_MODEL = prio
55
+ return prio
56
+ # Sinon: correspondance partielle
57
+ for m in models:
58
+ if 'nemotron' in m.lower():
59
+ NEMOTRON_MODEL = m
60
+ return m
61
+ except Exception:
62
+ pass
63
+ NEMOTRON_MODEL = OLLAMA_MODEL # fallback qwen si absent
64
+ return NEMOTRON_MODEL
65
+
66
+ detect_nemotron()
67
+
68
+ # ── Config FFmpeg ────────────────────────────────────────────
69
+ def find_ffmpeg():
70
+ candidates = [
71
+ os.path.join(os.path.expanduser('~'), 'ffmpeg', 'bin', 'ffmpeg.exe'),
72
+ os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'ffmpeg', 'bin', 'ffmpeg.exe'),
73
+ 'ffmpeg',
74
+ ]
75
+ for c in candidates:
76
+ if c == 'ffmpeg':
77
+ return c
78
+ if os.path.exists(c):
79
+ return c
80
+ return 'ffmpeg'
81
+
82
+ FFMPEG = find_ffmpeg()
83
+
84
+ # ── RΓ©sultat ─────────────────────────────────────────────────
85
+ def ok(**kwargs):
86
+ print(json.dumps({ 'ok': True, **kwargs }, ensure_ascii=False))
87
+ sys.exit(0)
88
+
89
+ def fail(msg):
90
+ print(json.dumps({ 'ok': False, 'error': str(msg) }, ensure_ascii=False))
91
+ sys.exit(1)
92
+
93
+
94
+ # ════════════════════════════════════════════════════════════
95
+ # OLLAMA β€” appel local
96
+ # ════════════════════════════════════════════════════════════
97
+ def ollama_generate(prompt, model=OLLAMA_MODEL):
98
+ try:
99
+ import urllib.request
100
+ body = json.dumps({
101
+ 'model': model,
102
+ 'prompt': prompt,
103
+ 'stream': False,
104
+ 'options': { 'temperature': 0, 'num_predict': 300 }
105
+ }).encode('utf-8')
106
+ req = urllib.request.Request(
107
+ OLLAMA_HOST + '/api/generate',
108
+ data=body,
109
+ headers={ 'Content-Type': 'application/json' },
110
+ method='POST'
111
+ )
112
+ with urllib.request.urlopen(req, timeout=30) as resp:
113
+ data = json.loads(resp.read().decode('utf-8'))
114
+ return data.get('response', '').strip()
115
+ except Exception as e:
116
+ return None
117
+
118
+
119
+ def generate_tags(name, media_type, content=''):
120
+ raw = ollama_generate(
121
+ f"Tu es un archiviste média. Génère exactement 5 tags pertinents pour cet asset.\n"
122
+ f"Nom: {name}\nType: {media_type}\nContenu: {content[:300] if content else 'non disponible'}\n"
123
+ f"RΓ©ponds UNIQUEMENT avec un tableau JSON de 5 strings en franΓ§ais. "
124
+ f"Ex: [\"tag1\",\"tag2\",\"tag3\",\"tag4\",\"tag5\"]"
125
+ )
126
+ if not raw:
127
+ return []
128
+ try:
129
+ import re
130
+ m = re.search(r'\[.*?\]', raw, re.DOTALL)
131
+ return json.loads(m.group(0)) if m else []
132
+ except Exception:
133
+ return []
134
+
135
+
136
+ def generate_summary(name, media_type, content=''):
137
+ return ollama_generate(
138
+ f"Tu es un archiviste mΓ©dia professionnel. RΓ©dige une description courte (2-3 phrases) pour:\n"
139
+ f"Nom: {name}\nType: {media_type}\nContenu: {content[:500] if content else 'non disponible'}\n"
140
+ f"RΓ©ponds uniquement avec la description en franΓ§ais."
141
+ )
142
+
143
+
144
+ # ════════════════════════════════════════════════════════════
145
+ # AUDIO β€” Whisper + conversion FLAC TC-04
146
+ # ════════════════════════════════════════════════════════════
147
+ def analyze_audio(file_path):
148
+ transcript = ''
149
+ flac_ok = False
150
+ flac_path = ''
151
+
152
+ # 1. Transcription Whisper
153
+ try:
154
+ import whisper
155
+ model = whisper.load_model('small')
156
+ result = model.transcribe(file_path, language=None, fp16=False)
157
+ transcript = result.get('text', '').strip()
158
+ except ImportError:
159
+ transcript = '[Whisper non installΓ© β€” lance INSTALLER_MAM.bat]'
160
+ except Exception as e:
161
+ transcript = f'[Erreur Whisper: {str(e)[:100]}]'
162
+
163
+ # 2. Conversion FLAC IASA TC-04 (96kHz / 24-bit)
164
+ try:
165
+ base = os.path.splitext(os.path.basename(file_path))[0]
166
+ flac_dir = os.path.join(os.path.dirname(file_path), '..', 'flac')
167
+ os.makedirs(flac_dir, exist_ok=True)
168
+ flac_path = os.path.abspath(os.path.join(flac_dir, base + '_TC04_96k24b.flac'))
169
+
170
+ cmd = [
171
+ FFMPEG, '-y', '-i', file_path,
172
+ '-c:a', 'flac',
173
+ '-ar', '96000',
174
+ '-sample_fmt', 's32',
175
+ '-bits_per_raw_sample', '24',
176
+ '-compression_level', '8',
177
+ flac_path
178
+ ]
179
+ result = subprocess.run(cmd, capture_output=True, timeout=120)
180
+ flac_ok = result.returncode == 0 and os.path.exists(flac_path)
181
+ except Exception as e:
182
+ flac_ok = False
183
+ flac_path = ''
184
+
185
+ # 3. Tags + rΓ©sumΓ© via Ollama
186
+ name = os.path.basename(file_path)
187
+ content = transcript[:400] if transcript else ''
188
+ tags = generate_tags(name, 'audio', content)
189
+ summary = generate_summary(name, 'audio', content)
190
+
191
+ ok(
192
+ summary = summary or f'Fichier audio : {name}',
193
+ tags = tags or ['audio', 'archive', 'patrimoine', 'son', 'ACoNum'],
194
+ transcript = transcript,
195
+ flac_ok = flac_ok,
196
+ flac_path = flac_path,
197
+ standard = 'IASA TC-04 β€” FLAC 96kHz/24-bit' if flac_ok else 'Conversion FLAC non effectuΓ©e',
198
+ )
199
+
200
+
201
+ # ════════════════════════════════════════════════════════════
202
+ # VIDEO β€” FFmpeg + Whisper
203
+ # ════════════════════════════════════════════════════════════
204
+ def analyze_video(file_path):
205
+ transcript = ''
206
+ audio_tmp = file_path + '_audio_tmp.wav'
207
+
208
+ # 1. Extraire l'audio en WAV
209
+ try:
210
+ cmd = [FFMPEG, '-y', '-i', file_path, '-vn',
211
+ '-ar', '16000', '-ac', '1', '-f', 'wav', audio_tmp]
212
+ subprocess.run(cmd, capture_output=True, timeout=120)
213
+ except Exception:
214
+ pass
215
+
216
+ # 2. Transcrire avec Whisper
217
+ audio_src = audio_tmp if os.path.exists(audio_tmp) else file_path
218
+ try:
219
+ import whisper
220
+ model = whisper.load_model('small')
221
+ result = model.transcribe(audio_src, language=None, fp16=False)
222
+ transcript = result.get('text', '').strip()
223
+ except ImportError:
224
+ transcript = '[Whisper non installΓ©]'
225
+ except Exception as e:
226
+ transcript = f'[Erreur Whisper: {str(e)[:100]}]'
227
+
228
+ # Nettoyage fichier temporaire
229
+ if os.path.exists(audio_tmp):
230
+ try: os.remove(audio_tmp)
231
+ except: pass
232
+
233
+ name = os.path.basename(file_path)
234
+ content = transcript[:400]
235
+ tags = generate_tags(name, 'video', content)
236
+ summary = generate_summary(name, 'video', content)
237
+
238
+ ok(
239
+ summary = summary or f'Fichier vidΓ©o : {name}',
240
+ tags = tags or ['video', 'archive', 'media', 'ACoNum', 'deepfake'],
241
+ transcript = transcript,
242
+ videoshield_url = 'https://huggingface.co/spaces/NOBODY204/VideoShield',
243
+ )
244
+
245
+
246
+ # ════════════════════════════════════════════════════════════
247
+ # IMAGE β€” LLaVA via Ollama (si disponible)
248
+ # ════════════════════════════════════════════════════════════
249
+ def analyze_image(file_path):
250
+ description = ''
251
+ name = os.path.basename(file_path)
252
+
253
+ # Tenter LLaVA via Ollama (modèle vision)
254
+ try:
255
+ import urllib.request, base64
256
+ with open(file_path, 'rb') as f:
257
+ img_b64 = base64.b64encode(f.read()).decode('utf-8')
258
+
259
+ body = json.dumps({
260
+ 'model': 'llava:7b',
261
+ 'prompt': 'DΓ©cris cette image en franΓ§ais en 2-3 phrases pour un archiviste mΓ©dia.',
262
+ 'images': [img_b64],
263
+ 'stream': False,
264
+ 'options': { 'temperature': 0, 'num_predict': 200 }
265
+ }).encode('utf-8')
266
+
267
+ req = urllib.request.Request(
268
+ OLLAMA_HOST + '/api/generate',
269
+ data=body,
270
+ headers={ 'Content-Type': 'application/json' },
271
+ method='POST'
272
+ )
273
+ with urllib.request.urlopen(req, timeout=60) as resp:
274
+ data = json.loads(resp.read().decode('utf-8'))
275
+ description = data.get('response', '').strip()
276
+ except Exception:
277
+ description = f'[LLaVA non disponible β€” analyse visuelle dΓ©sactivΓ©e pour {name}]'
278
+
279
+ tags = generate_tags(name, 'image', description)
280
+ summary = description or generate_summary(name, 'image', '')
281
+
282
+ ok(
283
+ summary = summary or f'Fichier image : {name}',
284
+ tags = tags or ['image', 'visuel', 'archive', 'media', 'ACoNum'],
285
+ description = description,
286
+ imageshield_url = 'https://huggingface.co/spaces/NOBODY204/ImageShield',
287
+ )
288
+
289
+
290
+ # ════════════════════════════════════════════════════════════
291
+ # PDF β€” pdfplumber
292
+ # ════════════════════════════════════════════════════════════
293
+ def analyze_pdf(file_path):
294
+ text = ''
295
+ name = os.path.basename(file_path)
296
+
297
+ try:
298
+ import pdfplumber
299
+ with pdfplumber.open(file_path) as pdf:
300
+ pages = []
301
+ for i, page in enumerate(pdf.pages[:5]): # 5 premières pages max
302
+ t = page.extract_text()
303
+ if t:
304
+ pages.append(t.strip())
305
+ text = '\n'.join(pages)
306
+ except ImportError:
307
+ text = '[pdfplumber non installΓ© β€” lance INSTALLER_MAM.bat]'
308
+ except Exception as e:
309
+ text = f'[Erreur lecture PDF: {str(e)[:100]}]'
310
+
311
+ content = text[:600]
312
+ tags = generate_tags(name, 'pdf', content)
313
+ summary = generate_summary(name, 'pdf', content)
314
+
315
+ ok(
316
+ summary = summary or f'Document PDF : {name}',
317
+ tags = tags or ['document', 'pdf', 'archive', 'texte', 'ACoNum'],
318
+ text = text[:1000] if text else '',
319
+ )
320
+
321
+
322
+ # ════════════════════════════════════════════════════════════
323
+ # MAIN
324
+ # ════════════════════════════════════════════════════════════
325
+ if __name__ == '__main__':
326
+ if len(sys.argv) < 3:
327
+ fail('Usage: python analyze_media.py <audio|video|image|pdf> <chemin_fichier>')
328
+
329
+ media_type = sys.argv[1].lower().strip()
330
+ file_path = sys.argv[2].strip()
331
+
332
+ if not os.path.exists(file_path):
333
+ fail(f'Fichier introuvable : {file_path}')
334
+
335
+ try:
336
+ if media_type == 'audio': analyze_audio(file_path)
337
+ elif media_type == 'video': analyze_video(file_path)
338
+ elif media_type == 'image': analyze_image(file_path)
339
+ elif media_type in ('pdf', 'doc'): analyze_pdf(file_path)
340
+ else: fail(f'Type non supportΓ© : {media_type}. Utiliser audio/video/image/pdf')
341
+ except Exception as e:
342
+ fail(traceback.format_exc())
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
 
345
 
346