From 67ff07820d0dfe030dd49eb6bf605992afc56f0b Mon Sep 17 00:00:00 2001 From: robjo82 Date: Fri, 23 Oct 2026 14:44:00 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(web):=20=E2=9C=A8=20Affiner=20les=20po?= =?UTF-8?q?ign=C3=A9es=20et=20les=20rep=C3=A8res=20audio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/README.md | 13 +- web/app/audio_parts.py | 28 ++ web/app/main.py | 3 + web/client/src/audio-parts.js | 14 + web/client/src/components/Apercu.jsx | 1 + web/client/src/components/Bibliotheque.jsx | 13 +- web/client/src/components/Detail.jsx | 6 +- web/client/src/components/FileOrganizer.jsx | 288 +++++++++--------- web/client/src/components/MediaPlayer.jsx | 63 +++- web/client/src/components/Nouveau.jsx | 45 +-- .../src/components/PreparationStatus.jsx | 23 +- web/client/src/fileQueue.js | 1 + web/client/src/merge-audio-worker.js | 4 +- web/client/src/merge-audio.js | 1 + web/client/src/organizer.js | 22 ++ web/client/src/preparation.js | 1 + web/client/src/styles.css | 34 +++ web/client/tests/README.md | 21 ++ web/client/tests/audio-parts.test.js | 19 ++ web/client/tests/browser.html | 6 +- web/client/tests/organizer.test.js | 28 +- web/docs/BACKLOG.md | 13 + .../screenshots/contextes-compacts-375.png | Bin 0 -> 43056 bytes .../screenshots/lecteur-morceaux-sombre.png | Bin 0 -> 31595 bytes web/docs/screenshots/lecteur-morceaux.png | Bin 0 -> 59997 bytes .../screenshots/organiser-corbeille-375.png | Bin 0 -> 41548 bytes web/docs/screenshots/organiser-corbeille.png | Bin 0 -> 61651 bytes web/tests/test_api.py | 14 + 28 files changed, 481 insertions(+), 180 deletions(-) create mode 100644 web/app/audio_parts.py create mode 100644 web/client/src/audio-parts.js create mode 100644 web/client/tests/audio-parts.test.js create mode 100644 web/docs/screenshots/contextes-compacts-375.png create mode 100644 web/docs/screenshots/lecteur-morceaux-sombre.png create mode 100644 web/docs/screenshots/lecteur-morceaux.png create mode 100644 web/docs/screenshots/organiser-corbeille-375.png create mode 100644 web/docs/screenshots/organiser-corbeille.png diff --git a/web/README.md b/web/README.md index 572f5d8..e9aee2e 100644 --- a/web/README.md +++ b/web/README.md @@ -152,6 +152,12 @@ Déposer plusieurs fichiers ouvre « Organiser les enregistrements ». Ils restent séparés par défaut. Avec la poignée, déposer un audio au centre d’un autre les réunit ; le déposer au bord change seulement l’ordre. On peut ajouter d’autres audios à un groupe et ordonner ses morceaux. +La poignée d’une réunion déplace tout le groupe ; celles des morceaux +changent seulement leur ordre interne. « Modifier les morceaux » ouvre +un éditeur limité à la réunion courante, sans toucher à la file. +Saisir une poignée révèle une corbeille temporaire : y déposer la réunion +ou le morceau le retire de la sélection. « Annuler le retrait » le restaure. +Les fichiers originaux ne sont jamais supprimés. Au clavier, Suppr retire. Au clavier, les flèches déplacent ; Entrée choisit la réunion, puis Entrée sur une autre poignée les réunit. Échap annule le choix. « Séparer » et « Annuler » permettent de revenir sur ce choix. « Organiser » rouvre la fenêtre depuis la file. @@ -161,11 +167,16 @@ du navigateur. Chaque morceau est normalisé en AAC mono 48 kHz, puis ses paquets rejoignent une piste unique sans second réencodage. Les originaux restent intacts ; les vidéos restent séparées. La transcription et l'archivage suivent ensuite le parcours habituel, pour une seule réunion. +Le lecteur marque les jonctions et propose un accès à chaque morceau. +Les repères utilisent les durées réellement encodées, suivent le rognage +et restent disponibles après archivage ou lors d’un retraitement. Les +anciennes réunions sans repères gardent le lecteur habituel. La file prépare les contextes en arrière-plan, une recherche d'avance à la fois. Le formulaire réutilise une recherche déjà terminée ou rejoint celle en cours. Aucun traitement ni dépôt Odoo ne démarre à ce stade. -Une recherche indisponible laisse la saisie manuelle possible. Changer +Une recherche indisponible laisse la saisie manuelle possible. Un pictogramme +par fichier et un résumé commun indiquent l’état des contextes, sans bulles. Changer l'ordre des morceaux crée un contexte distinct ; changer seulement l'ordre des réunions réutilise leurs contextes. diff --git a/web/app/audio_parts.py b/web/app/audio_parts.py new file mode 100644 index 0000000..643b2be --- /dev/null +++ b/web/app/audio_parts.py @@ -0,0 +1,28 @@ +"""Repères d’écoute du fichier assemblé, relatifs à la partie conservée.""" + +import json +import math + + +def audio_parts(context_json: str | None, duration: float) -> list[dict]: + """Une ancienne réunion ou des métadonnées invalides restent lisibles.""" + try: + context = json.loads(context_json or "{}") + parts = context.get("audio_parts", []) if isinstance(context, dict) else [] + if not isinstance(parts, list) or len(parts) > 200: + return [] + result, previous = [], 0.0 + for part in parts: + if not isinstance(part, dict): + return [] + name, start, end = part.get("name"), part.get("start"), part.get("end") + if (not isinstance(name, str) or not name or len(name) > 500 + or any(isinstance(n, bool) or not isinstance(n, (int, float)) + or not math.isfinite(n) for n in (start, end)) + or start < previous or end <= start or end > duration + 0.1): + return [] + result.append({"name": name, "start": start, "end": end}) + previous = end + return result + except (TypeError, ValueError): + return [] diff --git a/web/app/main.py b/web/app/main.py index 383024a..5d6fc63 100644 --- a/web/app/main.py +++ b/web/app/main.py @@ -44,6 +44,7 @@ provider_for_model, ) +from .audio_parts import audio_parts from . import avancement from .auth import AccessVerifier, AuthError from .chatter import ChatterError, composer @@ -1090,6 +1091,7 @@ def list_jobs( "status": job["status"], "model": job["model"], "duration_seconds": job["duration_seconds"], + "audio_parts": audio_parts(job["context_json"], job["duration_seconds"]), "cost_usd": job["cloud_cost_usd"], "created_at": job["created_at"], "has_versions": bool(job["previous_versions_json"]), @@ -1156,6 +1158,7 @@ def job_detail(job_id: int, owner_id: int = Depends(current_user)) -> dict: "reprocessable": job["status"] in REPROCESSABLE and not job["transcript"], "model": job["model"], "duration_seconds": job["duration_seconds"], + "audio_parts": audio_parts(job["context_json"], job["duration_seconds"]), "cost_usd": job["cloud_cost_usd"], "transcript": job["transcript"] or "", "speakers": json.loads(job["speaker_map_json"] or "{}"), diff --git a/web/client/src/audio-parts.js b/web/client/src/audio-parts.js new file mode 100644 index 0000000..11af64e --- /dev/null +++ b/web/client/src/audio-parts.js @@ -0,0 +1,14 @@ +/** Repères relatifs au fichier réellement écouté / archivé, rognage compris. */ +export function clipAudioParts(parts, start = 0, end = Infinity) { + if (!Array.isArray(parts) || !Number.isFinite(start) || start < 0 || !(end > start)) return []; + let previous = 0; + const result = []; + for (const part of parts) { + if (!part || !Number.isFinite(part.start) || !Number.isFinite(part.end) || + part.start < previous || part.end <= part.start || typeof part.name !== 'string') return []; + previous = part.end; + const left = Math.max(start, part.start), right = Math.min(end, part.end); + if (right > left) result.push({ name: part.name, start: left - start, end: right - start }); + } + return result; +} diff --git a/web/client/src/components/Apercu.jsx b/web/client/src/components/Apercu.jsx index 345a126..9dacf22 100644 --- a/web/client/src/components/Apercu.jsx +++ b/web/client/src/components/Apercu.jsx @@ -50,6 +50,7 @@ export function Apercu({ fichier, duree, debut, fin, surDebut, surFin, actif }) start={debut} end={fin} duration={duree} + parts={fichier.audioParts} onTrim={rogner} onUnreadable={() => setLisible(false)} inert={actif} diff --git a/web/client/src/components/Bibliotheque.jsx b/web/client/src/components/Bibliotheque.jsx index 2b0aa5a..d313189 100644 --- a/web/client/src/components/Bibliotheque.jsx +++ b/web/client/src/components/Bibliotheque.jsx @@ -6,7 +6,7 @@ import { MOD, useRaccourcis } from '../raccourcis.js'; import { surlignerMots } from '../surlignage.jsx'; import { Welcome } from './Welcome.jsx'; import { FileOrganizer } from './FileOrganizer.jsx'; -import { PreparationStatus } from './PreparationStatus.jsx'; +import { PreparationStatus, PreparationSummary } from './PreparationStatus.jsx'; import { uniqueFiles } from '../organizer.js'; import { interrompreReunion } from '../usePipeline.js'; import { @@ -599,18 +599,17 @@ function ALancer({ surLancer }) {

À lancer {enAttente.length}

- - Restés sur ton poste : la liste se vide si tu fermes l’onglet. - + entry.file)} /> {(enAttente.length > 1 || enAttente.some((entry) => entry.file.mergedParts)) && }