From 514ee164d1c563e80c7071d26f308befe820f5ca Mon Sep 17 00:00:00 2001 From: Antoine Van Elstraete Date: Sun, 22 Mar 2026 23:13:19 +0100 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20plan=20d'impl=C3=A9mentation=20AV1?= =?UTF-8?q?-SVT=20+=20Opus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/superpowers/plans/2026-03-22-av1-opus.md | 697 ++++++++++++++++++ 1 file changed, 697 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-22-av1-opus.md diff --git a/docs/superpowers/plans/2026-03-22-av1-opus.md b/docs/superpowers/plans/2026-03-22-av1-opus.md new file mode 100644 index 0000000..153ea42 --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-av1-opus.md @@ -0,0 +1,697 @@ +# Migration AV1-SVT + Opus Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal :** Migrer `vid_convert.py` de H.265/AAC-LC vers AV1-SVT/Opus avec sélection automatique du CRF via `ab-av1 crf-search`, sortie MKV, et support HDR10/HDR10+/Dolby Vision complet. + +**Architecture :** Script Python mono-fichier modifié en place. Les fonctions obsolètes (`mkv_to_mp4`, `is_interlaced`) sont supprimées, la nouvelle fonction `find_crf` est ajoutée, et toutes les fonctions existantes sont mises à jour. Le flux principal perd la logique de chunking. + +**Tech Stack :** Python 3, FFmpeg (libsvtav1, libopus), ab-av1, hdr10plus_parser, mkvmerge + +**Spec :** `docs/superpowers/specs/2026-03-22-av1-opus-design.md` + +--- + +## Chunk 1 : Nettoyage, corrections et audio Opus + +### Task 1 : Correction du bug et suppression du code obsolète + +**Fichiers :** +- Modifier : `vid_convert.py` + +- [ ] **Étape 1 : Décommenter `get_infos` et supprimer la ligne orpheline** + +Dans `vid_convert.py`, ligne 308, remplacer : +```python + # infos = get_infos(file) + interlaced = False + interlaced = is_interlaced(file, infos) +``` +Par : +```python + infos = get_infos(file) +``` + +- [ ] **Étape 2 : Supprimer la fonction `is_interlaced`** + +Supprimer les lignes 81–104 (fonction `is_interlaced` entière). + +- [ ] **Étape 3 : Supprimer la fonction `mkv_to_mp4`** + +Supprimer les lignes 283–290 (fonction `mkv_to_mp4` entière). + +- [ ] **Étape 4 : Mettre à jour les arguments CLI** + +Dans le bloc `__main__`, remplacer : +```python + parser.add_argument("-t", "--starttime", dest="starttime") + parser.add_argument("-a", "--animation", dest="animation", action="store_true") + parser.add_argument("-c", "--vhs", dest="vhs", action="store_true") +``` +Par : +```python + parser.add_argument("-a", "--animation", dest="animation", action="store_true") + parser.add_argument("-c", "--vhs", dest="vhs", action="store_true") + parser.add_argument("--interlaced", dest="interlaced", action="store_true") +``` + +- [ ] **Étape 5 : Supprimer le flux principal obsolète** + +> ⚠️ Après cette étape, le script sera syntaxiquement valide mais non fonctionnel. Il sera complété au Task 8. + +Dans le bloc `__main__`, supprimer exactement ce bloc (état du fichier après Step 1, à partir de `file = args.f_input` jusqu'à la fin du fichier) : +```python + file = args.f_input + infos = get_infos(file) + cropsize = cropping(file, infos) + volumes = volume_audio(file, infos) + if args.stab: + stabilization(file) + if args.animation: + animation = True + else: + animation = False + if not args.starttime: + for track in infos['subtitles']: + extract_subs(file, track['index'], track['language']) + for track in infos['audio']: + convert_audio(file, track['index'], volumes[track['index']], track['channels'], track['channel_layout'], track['language'], track['title']) + if args.starttime: + vid_part_time = int(args.starttime) + else: + vid_part_time = 0 + while vid_part_time < infos['duration']: + crf = 20 + convert_video(file, infos, vid_part_time, cropsize, crf, animation, interlaced, args.vhs) + vid_part_time += 300 + create_mkv(file) + mkv_to_mp4(file) +``` + +Remplacer par un placeholder temporaire (sera remplacé au Task 8) : +```python + pass +``` + +- [ ] **Étape 6 : Vérifier que le script parse sans erreur** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 7 : Commit** + +```bash +git add vid_convert.py +git commit -m "refactor: suppression code obsolète (chunks, is_interlaced, mkv_to_mp4, -t)" +``` + +--- + +### Task 2 : Encodage audio — libfdk_aac → libopus + +**Fichiers :** +- Modifier : `vid_convert.py` (fonction `convert_audio`) + +- [ ] **Étape 1 : Vérifier que libopus est disponible dans FFmpeg** + +```bash +ffmpeg -codecs 2>/dev/null | grep -i opus +``` +Résultat attendu : une ligne contenant `libopus` et `A..... libopus`. Si absent, libopus doit être compilé dans FFmpeg avant de continuer. + +- [ ] **Étape 2 : Remplacer la fonction `convert_audio`** + +Remplacer l'intégralité de la fonction `convert_audio` par : +```python +def convert_audio(file, track, volume_adj, channels, channel_layout, language, title): + if channel_layout == "5.1(side)": + channel_layout = "5.1" + if channels <= 2: + bitrate = "128k" + elif channels == 6: + bitrate = "320k" + else: + bitrate = "450k" + codec = f'libopus -vbr on -b:a {bitrate}' + metadatas = f'-metadata language="{language}" -metadata title="{title}"' + command = f'ffmpeg -loglevel error -i {file} -map 0:{track} -map_metadata -1 -vn -sn -c:a {codec} -mapping_family 1 -filter:a volume={volume_adj},aformat=channel_layouts={channel_layout} {metadatas} -y {file}_audio_{track}_{language}.mka' + logging.debug(command) + result = subprocess.getoutput(command) + if result != "": + logging.error(result) +``` + +- [ ] **Étape 3 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 4 : Commit** + +```bash +git add vid_convert.py +git commit -m "feat: encodage audio libopus VBR avec bitrate adaptatif" +``` + +--- + +## Chunk 2 : Sélection CRF et encodage vidéo AV1 + +### Task 3 : Nouvelle fonction `find_crf` + +**Fichiers :** +- Modifier : `vid_convert.py` (ajout de fonction avant `convert_video`) + +- [ ] **Étape 1 : Ajouter la fonction `find_crf` dans `vid_convert.py`** + +- [ ] **Étape 1 : Ajouter `import os` dans les imports du module** + +En haut du fichier, après `from os import listdir, remove`, ajouter : +```python +import os +``` + +- [ ] **Étape 2 : Ajouter la fonction `find_crf`** + +Insérer après la fonction `volume_audio` et avant `stabilization` : +```python +VMAF_HDR_MODEL = '/usr/share/vmaf/model/vmaf_4k_v0.6.1.json' + +def find_crf(file, enc_options, hdr=False): + ''' + Détermine le CRF optimal via ab-av1 crf-search pour atteindre VMAF 96. + Pour les sources HDR, utilise un modèle VMAF calibré 4K HDR si disponible. + ''' + logging.info("Recherche du CRF optimal (VMAF 96)...") + vmaf_model = '' + if hdr and os.path.exists(VMAF_HDR_MODEL): + vmaf_model = f'--vmaf-model path={VMAF_HDR_MODEL}' + logging.debug(f"Modèle VMAF HDR utilisé : {VMAF_HDR_MODEL}") + cmd = f'ab-av1 crf-search --input {file} --encoder libsvtav1 --vmaf 96 {vmaf_model} --enc {enc_options}' + logging.debug(cmd) + result = subprocess.getoutput(cmd) + logging.debug(result) + for line in result.splitlines(): + # ab-av1 émet une ligne du type : "crf 32 VMAF 96.21 ..." + if 'crf' in line and 'VMAF' in line: + try: + crf = int(line.split('crf')[1].split()[0]) + logging.info(f"CRF optimal trouvé : {crf}") + return crf + except (IndexError, ValueError): + pass + logging.warning("ab-av1 crf-search a échoué, utilisation du CRF par défaut (32)") + return 32 +``` + +- [ ] **Étape 3 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 4 : Commit** + +```bash +git add vid_convert.py +git commit -m "feat: ajout find_crf via ab-av1 crf-search (VMAF 96)" +``` + +--- + +### Task 4 : Encodage vidéo — libx265 → libsvtav1 + +**Fichiers :** +- Modifier : `vid_convert.py` (fonction `convert_video`) + +- [ ] **Étape 1 : Remplacer la signature et les variables de base** + +Remplacer le début de `convert_video` : +```python +def convert_video(file, infos, start, crop, crf, animation, interlaced, vhs): + str_start = "{:05d}".format(start) + output = f'{file}_video_t{str_start}.mkv' + fmt = "yuv420p10le" + track = infos['video']['index'] + codec = 'libx265 -preset slower' + hdr = '' + if animation: + tune = "-tune animation" + else: + tune = "" + if interlaced: + crop = f"{crop},yadif" + if vhs: + crop = f"{crop},hqdn3d,unsharp=5:5:0.8:3:3:0.4" +``` +Par : +```python +def convert_video(file, infos, crf, crop, enc_options, interlaced, vhs): + output = f'{file}_video.mkv' + fmt = "yuv420p10le" + track = infos['video']['index'] + codec = 'libsvtav1' + svtav1_params = enc_options + hdr = '' + if interlaced: + crop = f"{crop},yadif" + if vhs: + crop = f"{crop},hqdn3d,unsharp=5:5:0.8:3:3:0.4" +``` + +- [ ] **Étape 2 : Mettre à jour le bloc HDR10 statique** + +Remplacer le bloc `if 'side_data_list' in infos['video'].keys():` jusqu'à sa fin : +```python + if 'side_data_list' in infos['video'].keys(): + try: + light_level = f"{infos['video']['side_data_list'][1]['max_content']},{infos['video']['side_data_list'][1]['max_average']}" + color_primaries = infos['video']['color_primaries'] + color_transfer = infos['video']['color_transfer'] + color_space = infos['video']['color_space'] + green_x = infos['video']['side_data_list'][0]['green_x'].split('/') + green_x = int(int(green_x[0])*(int(green_x[1])/50000)) + green_y = infos['video']['side_data_list'][0]['green_y'].split('/') + green_y = int(int(green_y[0])*(int(green_y[1])/50000)) + green = f'G\({green_x},{green_y}\)' + blue_x = infos['video']['side_data_list'][0]['blue_x'].split('/') + blue_x = int(int(blue_x[0])*(int(blue_x[1])/50000)) + blue_y = infos['video']['side_data_list'][0]['blue_y'].split('/') + blue_y = int(int(blue_y[0])*(int(blue_y[1])/50000)) + blue = f'B\({blue_x},{blue_y}\)' + red_x = infos['video']['side_data_list'][0]['red_x'].split('/') + red_x = int(int(red_x[0])*(int(red_x[1])/50000)) + red_y = infos['video']['side_data_list'][0]['red_y'].split('/') + red_y = int(int(red_y[0])*(int(red_y[1])/50000)) + red = f'R\({red_x},{red_y}\)' + white_point_x = infos['video']['side_data_list'][0]['white_point_x'].split('/') + white_point_x = int(int(white_point_x[0])*(int(white_point_x[1])/50000)) + white_point_y = infos['video']['side_data_list'][0]['white_point_y'].split('/') + white_point_y = int(int(white_point_y[0])*(int(white_point_y[1])/50000)) + white_point = f'WP\({white_point_x},{white_point_y}\)' + min_luminance = infos['video']['side_data_list'][0]['min_luminance'].split('/') + min_luminance = int(int(min_luminance[0])*(int(min_luminance[1])/10000)) + max_luminance = infos['video']['side_data_list'][0]['max_luminance'].split('/') + max_luminance = int(int(max_luminance[0])*(int(max_luminance[1])/10000)) + luminance = f'L\({max_luminance},{min_luminance}\)' + master_display = green + blue + red + white_point + luminance + hdr = f'mastering-display={master_display}:content-light={light_level}' + if svtav1_params: + svtav1_params = f'{svtav1_params}:{hdr}' + else: + svtav1_params = hdr + except Exception as err: + logging.debug(f"Aucune information HDR statique : {err}") +``` + +- [ ] **Étape 3 : Mettre à jour l'injection HDR10+ et la commande FFmpeg finale** + +Remplacer la fin de `convert_video` (bloc HDR10+ + commande) : +```python + if 'hdr10plus' in infos['video']: + hdr10plus_param = f"hdr10plus-json={infos['video']['hdr10plus_metadata']}" + if svtav1_params: + svtav1_params = f'{svtav1_params}:{hdr10plus_param}' + else: + svtav1_params = hdr10plus_param + svtav1_args = f'-svtav1-params {svtav1_params}' if svtav1_params else '' + command = f'ffmpeg -loglevel error -i {file} -map 0:{track} -an -sn -c:v {codec} {svtav1_args} -crf {crf} -pix_fmt {fmt} -filter:v {crop} -y {output}' + logging.info("Encodage vidéo en cours (AV1-SVT)...") + logging.debug(command) + result = subprocess.getoutput(command) + if result != "": + logging.error(result) +``` + +- [ ] **Étape 4 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 5 : Commit** + +```bash +git add vid_convert.py +git commit -m "feat: encodage vidéo libsvtav1 avec HDR10 statique et HDR10+ via svtav1-params" +``` + +--- + +## Chunk 3 : HDR10+, Dolby Vision, MKV et flux principal + +### Task 5 : Détection du conteneur source pour HDR10+ + +**Fichiers :** +- Modifier : `vid_convert.py` (fonction `get_infos`) + +- [ ] **Étape 1 : Détecter le format du conteneur dans `get_infos`** + +Dans `get_infos`, remplacer le bloc de récupération de la durée ET le bloc d'extraction HDR10+ (les deux appels ffprobe sont fusionnés en un seul) : + +Remplacer : +```python + duration = subprocess.getoutput(f"ffprobe -v quiet -print_format json -show_format {file}") + duration = json.loads(duration) + duration = float(duration['format']['duration']) + hdr10_v_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser -o metadata.json --verify -' + hdr10_v_raw = subprocess.getoutput(hdr10_v_cmd) + if 'metadata detected' in hdr10_v_raw: + hdr10_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser -o /tmp/{file}_hdr10_metadata.json -' + hdr10_cmd_res = subprocess.getoutput(hdr10_cmd) + logging.debug(hdr10_cmd_res) + v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'}) +``` +Par : +```python + format_data = json.loads(subprocess.getoutput(f"ffprobe -v quiet -print_format json -show_format {file}")) + duration = float(format_data['format']['duration']) + format_name = format_data.get('format', {}).get('format_name', '') + is_mp4_container = 'mp4' in format_name or 'mov' in format_name + bsf = '-vbsf hevc_mp4toannexb' if is_mp4_container else '' + hdr10_v_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy {bsf} -f hevc - | hdr10plus_parser --verify -' + hdr10_v_raw = subprocess.getoutput(hdr10_v_cmd) + if 'metadata detected' in hdr10_v_raw: + hdr10_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy {bsf} -f hevc - | hdr10plus_parser -o /tmp/{file}_hdr10_metadata.json -' + hdr10_cmd_res = subprocess.getoutput(hdr10_cmd) + logging.debug(hdr10_cmd_res) + v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'}) +``` + +- [ ] **Étape 2 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 3 : Commit** + +```bash +git add vid_convert.py +git commit -m "fix: détection conteneur source pour extraction HDR10+ (MKV vs MP4)" +``` + +--- + +### Task 6 : Détection Dolby Vision + +**Fichiers :** +- Modifier : `vid_convert.py` (fonction `get_infos`) + +- [ ] **Étape 1 : Ajouter la détection DV après le bloc HDR10+** + +Dans `get_infos`, insérer le code suivant **au niveau d'indentation de la fonction** (pas à l'intérieur du `if`), après la ligne `v_infos.update({'hdr10plus': True, ...})` et avant la ligne `infos = {'duration': duration, ...}`. + +Concrètement, le code s'insère entre ces deux ancres exactes : +```python + v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'}) + # ← fin du bloc if 'metadata detected' (dés-indentation ici) + infos = {'duration': duration, 'video': v_infos, 'audio': a_infos, 'subtitles': s_infos} +``` + +Insérer (au niveau d'indentation de la fonction, soit 4 espaces) : +```python + try: + for side_data in full_v_infos['frames'][0].get('side_data_list', []): + if side_data.get('side_data_type') == 'DOVI configuration record': + dv_profile = side_data.get('dv_profile', '?') + logging.warning( + f"Dolby Vision détecté (Profile {dv_profile}). " + f"Encodage sans couche DV — HDR10 utilisé si disponible." + ) + v_infos.update({'dolby_vision_profile': dv_profile}) + break + except (KeyError, IndexError): + pass +``` + +- [ ] **Étape 2 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 3 : Commit** + +```bash +git add vid_convert.py +git commit -m "feat: détection Dolby Vision avec avertissement (fallback HDR10)" +``` + +--- + +### Task 7 : Simplification de `create_mkv` + +**Fichiers :** +- Modifier : `vid_convert.py` (fonction `create_mkv`) + +- [ ] **Étape 1 : Réécrire `create_mkv` pour une seule piste vidéo** + +Remplacer l'intégralité de la fonction `create_mkv` par : +```python +def create_mkv(filename): + json_data = [] + json_data.append("--output") + json_data.append(f"NEW_{filename}.mkv") + video_file = f"{filename}_video.mkv" + json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters", + "(", video_file, ")"] + for file in listdir(): + if f"{filename}_audio" in file: + lang = file[-7:][:-4] + json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters", + "--language", f"0:{lang}", + "(", file, ")"] + for file in listdir(): + if f"{filename}_subtitle" in file: + lang = file[-7:][:-4] + json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters", + "--language", f"0:{lang}", + "(", file, ")"] + with open(f"/tmp/{filename}.json", "w") as mkvmerge_options: + mkvmerge_options.write(json.dumps(json_data)) + command = f"mkvmerge -v @/tmp/{filename}.json" + logging.debug(command) + result = subprocess.getoutput(command) + logging.info(result) + remove(f"/tmp/{filename}.json") + for file in listdir(): + if file == video_file: + remove(file) + if f"{filename}_audio" in file: + remove(file) + if f"{filename}_subtitle" in file: + remove(file) +``` + +- [ ] **Étape 2 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 3 : Commit** + +```bash +git add vid_convert.py +git commit -m "refactor: create_mkv simplifié — une seule piste vidéo, sortie NEW_{filename}.mkv" +``` + +--- + +### Task 8 : Mise à jour du flux principal (`__main__`) + +**Fichiers :** +- Modifier : `vid_convert.py` (bloc `__main__`) + +- [ ] **Étape 1 : Réécrire le flux principal** + +Remplacer tout le contenu du bloc `__main__` à partir de `file = args.f_input` jusqu'à la fin du fichier par : +```python + file = args.f_input + infos = get_infos(file) + if args.stab: + stabilization(file) + cropsize = cropping(file, infos) + volumes = volume_audio(file, infos) + enc_options = 'preset=3:tune=0' + is_hdr = 'side_data_list' in infos['video'] or 'hdr10plus' in infos['video'] + crf = find_crf(file, enc_options, hdr=is_hdr) + for track in infos['subtitles']: + extract_subs(file, track['index'], track['language']) + for track in infos['audio']: + convert_audio(file, track['index'], volumes[track['index']], track['channels'], + track['channel_layout'], track['language'], track['title']) + convert_video(file, infos, crf, cropsize, enc_options, args.interlaced, args.vhs) + create_mkv(file) +``` + +Note : `tune=0` (VQ) est utilisé pour tous les contenus y compris l'animation, conformément à la spec. + +- [ ] **Étape 2 : Vérifier le parsing** + +```bash +python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')" +``` +Résultat attendu : `OK` + +- [ ] **Étape 3 : Vérifier l'aide CLI** + +```bash +python3 vid_convert.py --help +``` +Résultat attendu : affichage des arguments `f_input`, `-d`, `-s`, `-a`, `-c`, `--interlaced` — sans `-t`. + +- [ ] **Étape 4 : Commit** + +```bash +git add vid_convert.py +git commit -m "feat: flux principal AV1+Opus — find_crf, encode one-pass, sortie MKV" +``` + +--- + +### Task 9 : Mise à jour du README et du CLAUDE.md + +**Fichiers :** +- Modifier : `README.md` +- Modifier : `CLAUDE.md` + +- [ ] **Étape 1 : Mettre à jour README.md** + +Remplacer (noter l'espace de fin en fin de ligne 2 : `...avec un audio en `) : +``` +vid_convert est un script qui utilise ffmpeg pour convertir des vidéos au +format [H.265](https://fr.wikipedia.org/wiki/H.265/HEVC), avec un audio en +[AAC-LC](https://fr.wikipedia.org/wiki/Advanced_Audio_Coding). +``` +Par : +``` +vid_convert est un script qui utilise ffmpeg pour convertir des vidéos au +format [AV1](https://fr.wikipedia.org/wiki/AV1), avec un audio en +[Opus](https://fr.wikipedia.org/wiki/Opus_(codec)). +``` + +Remplacer : +``` +Le format de sortie est un fichier MPEG-4 (.mp4), lisible sur n'importe quel +appareil (ou presque) disposant d'une puissance de décodage suffisante. +``` +Par : +``` +Le format de sortie est un fichier Matroska (.mkv), optimisé pour l'archivage +de demux DVD et BluRay. +``` + +Remplacer la section dépendances : +``` + - [Python](https://www.python.org/) (>= 3.5) + - [ffmpeg](https://ffmpeg.org/) + - [hdr10plus_tool](https://github.com/quietvoid/hdr10plus_tool) +``` +Par : +``` + - [Python](https://www.python.org/) (>= 3.5) + - [ffmpeg](https://ffmpeg.org/) (avec libsvtav1 et libopus) + - [ab-av1](https://github.com/alexheretic/ab-av1) + - hdr10plus_parser (binaire inclus dans le dépôt) +``` + +- [ ] **Étape 2 : Mettre à jour CLAUDE.md — section Projet** + +Remplacer : +``` +Script Python standalone de conversion vidéo vers H.265/AAC-LC en conteneur MP4, avec support HDR10+, désentrelacement, détection de bandes noires, et normalisation audio. +``` +Par : +``` +Script Python standalone de conversion vidéo vers AV1/Opus en conteneur MKV, avec support HDR10/HDR10+/Dolby Vision (fallback HDR10), sélection automatique du CRF via ab-av1, détection de bandes noires, et normalisation audio. Cible : archivage de demux DVD/BluRay. +``` + +- [ ] **Étape 3 : Mettre à jour CLAUDE.md — section Utilisation** + +Remplacer : +``` +./vid_convert.py -t # reprendre à partir d'une position +``` +Par : +``` +./vid_convert.py --interlaced # forcer le désentrelacement (yadif) +``` + +- [ ] **Étape 4 : Mettre à jour CLAUDE.md — section Dépendances** + +Remplacer : +``` +- `ffmpeg` et `ffprobe` — encodage et extraction de métadonnées +- `mkvmerge` (MkvToolNix) — assemblage du conteneur intermédiaire +- `hdr10plus_tool` (binaire inclus : `hdr10plus_parser`) — métadonnées HDR10+ +``` +Par : +``` +- `ffmpeg` et `ffprobe` — encodage et extraction de métadonnées (libsvtav1 et libopus requis) +- `ab-av1` — sélection automatique du CRF par VMAF +- `mkvmerge` (MkvToolNix) — assemblage du conteneur MKV final +- `hdr10plus_parser` (binaire inclus) — extraction des métadonnées HDR10+ +``` + +- [ ] **Étape 5 : Mettre à jour CLAUDE.md — section Architecture** + +Remplacer l'intégralité de la section Architecture (de la ligne `Fichier unique` jusqu'à `deux passes`) par : +``` +Fichier unique : `vid_convert.py`. Flux d'exécution séquentiel : + +1. **`get_infos(file)`** — ffprobe JSON → métadonnées vidéo/audio/sous-titres + détection HDR10+/DV +2. **`cropping(file, infos)`** — cropdetect FFmpeg → suppression des bandes noires +3. **`volume_audio(file, infos)`** — volumedetect FFmpeg → ajustement par piste audio +4. **`find_crf(file, enc_options, hdr)`** — ab-av1 crf-search VMAF 96 → CRF optimal +5. **`extract_subs(file, track, lang)`** — extraction des pistes de sous-titres +6. **`convert_audio(...)`** — encodage libopus VBR par piste +7. **`convert_video(...)`** — encodage libsvtav1 CRF=auto preset=3, passe unique +8. **`create_mkv(filename)`** — mkvmerge → MKV final `NEW_{filename}.mkv` + +### Points clés + +- Sortie vidéo : `yuv420p10le` (10 bits), HDR10 via `-svtav1-params`, HDR10+ via `hdr10plus-json` +- `find_crf` utilise le modèle VMAF HDR (`vmaf_4k_v0.6.1.json`) si disponible sur le système +- Les fichiers temporaires passent par `/tmp/` (métadonnées HDR10+, options mkvmerge JSON) +- Dolby Vision Profile 8 → fallback HDR10 automatique ; Profile 5 → avertissement +- Le filtre VHS combine `hqdn3d` + `unsharp` +- La stabilisation utilise `vidstabdetect` + `vidstabtransform` en deux passes +``` + +- [ ] **Étape 6 : Mettre à jour CLAUDE.md — section Gotchas** + +Remplacer l'intégralité de la section Gotchas par : +``` +## Gotchas + +- **libsvtav1 et libopus requis** : le binaire FFmpeg doit être compilé avec `--enable-libsvtav1` et `--enable-libopus`. Vérifier avec `ffmpeg -codecs | grep -E 'svtav1|opus'`. +- **ab-av1 requis** : doit être installé et disponible dans le PATH. Voir https://github.com/alexheretic/ab-av1. +- **Fichiers temporaires orphelins** : en cas d'interruption, `{fichier}_video.mkv`, `{fichier}_audio_*.mka` et fichiers HDR10+ peuvent rester dans le répertoire courant ou `/tmp/`. Les supprimer manuellement si besoin. +- **Fichier de sortie** : `NEW_{nom_source}.mkv` généré dans le même répertoire que la source. +``` + +- [ ] **Étape 7 : Vérification** + +```bash +grep -c "AV1\|Opus\|MKV\|ab-av1" README.md +grep -c "libsvtav1\|libopus\|ab-av1\|find_crf" CLAUDE.md +``` +Résultat attendu : chaque commande retourne un entier > 0. + +- [ ] **Étape 8 : Commit** + +```bash +git add README.md CLAUDE.md +git commit -m "docs: mise à jour README et CLAUDE.md pour AV1+Opus+MKV" +```