release: v0.1.6 — corpus UI, exceptions, rename cascade, export destination

This commit is contained in:
Axelle Abbadie 2026-05-17 13:59:35 +02:00
commit a3204f1f76
9 changed files with 240 additions and 49 deletions

View file

@ -1,5 +1,57 @@
# Changelog
## [prod] v0.1.6 — 17 mai 2026
### Nouvelles fonctionnalités
**Onglet Corpus**
- Nouvel onglet **Corpus** (dernier dans le panneau) — transcriptions par classe, badges statut : règles, version pseudo, format d'export final
- **Gestion des classes** : créer, supprimer (déplace les fichiers à la racine)
- **Déplacer des fichiers entre classes** : dropdown par fichier — déplace `.md` + `.mapping.json` + `.words.json` + audio + fichiers source en miroir
- **Paramètres de destination des exports finaux** (inline dans l'onglet) : dossier vault (avec option miroir de classes), à côté du fichier source, ou dossier externe ; `FolderSuggest` autocomplete sur tous les champs de dossier
- Composant `FolderSuggest` — autocomplete dossiers vault dans les Paramètres et le wizard
**Modale de scan — candidats par occurrence**
- `MappingScanReviewModal` : colonne occurrences cliquable (`N / total` si exceptions)
- `OccurrencesContextModal` : cartes par occurrence (✓ / ✗ / ⚠) avec contexte, callback "Confirmer la sélection", sans application immédiate
- **Bouton "Enregistrer les exceptions"** : persiste les décisions ignorées dans `mapping.json` sans pseudonymiser
- Pré-remplit les décisions depuis `ignoredOccurrences` existantes à la réouverture
**Exceptions**
- `IgnoredOccurrence {text, contextBefore, contextAfter}` sur `MappingRule.ignoredOccurrences`
- Persistance dans `mapping.json`, chargée au démarrage — rouge sans re-scan
- Surlignage rouge : positionnel, par contexte (`findByContext`) — seulement l'occurrence précise est rouge
- `findSpansForRule()` exclut les occurrences ignorées du remplacement à l'export
- Clic droit "Déclarer une exception ici" sur les termes orange (contexte 30 caractères)
- Section Exceptions dans l'onglet Mappings avec cartes contextuelles et suppression
**Exports**
- `markdownToSrt()` et `markdownToCha()` — re-export du Markdown pseudonymisé en SRT/CHAT
- `resolveExportPath()` — miroir de classes depuis le dossier mapping quand le fichier est dans exports
- `writeExport()` — vault, à côté du source, ou externe (Node.js fs)
- Onglet Exports : bouton adapté selon source ou pseudonymisé ; masque pseudonymiser pour les fichiers déjà exportés
**Avertissement nom de fichier**
- Bannière au-dessus des onglets si le nom contient un terme pseudonymisable
- Deux boutons : ✏ renommage manuel · ✨ suggestion neutre (`transcript_N`)
- Statuts clarifiés : "Actif / Partiel / Ignoré / Suggéré"
- `getValidatedFor()` inclut `partial` comme état actif
**Cascade de renommage**
- Écouteur `vault.on('rename')` — cascade automatique au renommage natif Obsidian
- `cascadeRelatedRename()` — met à jour : frontmatter (`processFrontMatter`), `.mapping.json` (scope store + règles), `.words.json`, exports pseudonymisés, audio (même basename + référence `pseudobs-audio`), fichiers source (.srt/.vtt/.cha/.html/.yml)
- `moveFileToClass()` déplace aussi l'audio et met à jour les deux niveaux de scope
- Guard `_renamingRelated` prévient les boucles
### Corrections
- `renameFileAndRelated` : `oldFilePath` capturé avant `vault.rename()` (TFile muté in-place)
- Renommage audio : `oldAudioName` capturé avant rename (même problème de mutation)
- `processFrontMatter` pour toutes les modifications de frontmatter (regex peu fiable)
- Scope store ET scope des règles mis à jour lors du déplacement/renommage
- `esbuild` : `copyStylesPlugin` copie `styles.css` dans le vault de test à chaque rebuild dev
---
## [prod] v0.1.5 — 17 mai 2026
### Nouvelles fonctionnalités

View file

@ -2,6 +2,61 @@
> Previous entries in French: [CHANGELOG.fr.md](CHANGELOG.fr.md)
## [prod] v0.1.6 — 17 May 2026
### New features
**Corpus tab**
- New **Corpus** tab (last in the panel) — transcription files organized by class with status badges: rule count, pseudonymized version, final export format
- **Class management**: create, rename (coming), delete classes (moves files to root)
- **Move files between classes**: dropdown per file — moves `.md` + `.mapping.json` + `.words.json` + audio + source files in sync
- **Export destination settings** (inline in the tab): vault folder (with optional class-structure mirroring), next to source, or external folder; `FolderSuggest` autocomplete on all folder fields
- `FolderSuggest` component — folder autocomplete in Settings and Onboarding wizard
**Scan modal — per-occurrence candidates**
- `MappingScanReviewModal`: occurrence count column is now a clickable button showing `N / total` when some are ignored
- `OccurrencesContextModal`: per-occurrence cards (✓ / ✗ / ⚠) with context, "Confirm selection" callback, no immediate application
- **"Save exceptions" button**: persists ignored decisions in `mapping.json` without pseudonymizing
- Pre-fills decisions from existing `ignoredOccurrences` on re-open
- `MappingRuleResult` now carries `occurrences: Occurrence[]`
**Exceptions**
- `IgnoredOccurrence {text, contextBefore, contextAfter}` on `MappingRule.ignoredOccurrences`
- Persisted in `mapping.json`, loaded at startup — red highlighting without re-scanning
- Red highlighting: context-aware, position-based via `findByContext()` — only the specific ignored occurrence is red, not all occurrences of the word
- `findSpansForRule()` skips ignored occurrences — they are not pseudonymized on export
- Right-click "Declare as exception here" on orange terms (captures 30-char context)
- Exceptions section in Mappings tab with context cards and delete button
**Exports**
- `markdownToSrt()` and `markdownToCha()` — re-export pseudonymized Markdown back to SRT/CHAT
- `resolveExportPath()` — mirroring class structure from mapping folder when file is in exports
- `writeExport()` — vault, next-to-source, or external (Node.js fs); ensures parent folder
- Exports tab: button adapts based on whether active file is source or pseudonymized export; hides pseudonymize button for files already in exports folder
**Filename warning**
- Banner above the tab bar when active file's name contains a pseudonymizable term
- Two buttons: ✏ manual rename prompt · ✨ apply neutral suggestion (`transcript_N`)
- `suggestCorrectedFilename()` — neutral naming, auto-incremented, no replacement leak
- Status labels clarified: "Active / Partial / Ignored / Suggested"
- `getValidatedFor()` includes `partial` as active
**Rename cascade**
- `vault.on('rename')` listener — automatic cascade when user renames via Obsidian explorer
- `cascadeRelatedRename()` — updates: frontmatter (`app.fileManager.processFrontMatter`), `.mapping.json` (store scope + rule scopes), `.words.json`, pseudonymized exports, audio (same basename + `pseudobs-audio` reference), source files (.srt/.vtt/.cha/.html/.yml)
- Audio strategy: renames audio with same basename AND audio referenced by `pseudobs-audio` (noScribe, different basename)
- Guard flag `_renamingRelated` prevents cascade loops
- `moveFileToClass()` also moves audio and updates both store-level and rule-level scope paths
### Fixes
- `renameFileAndRelated`: capture `oldFilePath` before `vault.rename()` (TFile mutated in-place)
- Audio rename: capture `oldAudioName` before rename (same mutation issue)
- `processFrontMatter` for all frontmatter updates (regex was unreliable)
- Both store-level `scope.path` and individual rule `scope.path` updated on move/rename
- `esbuild`: `copyStylesPlugin` copies `styles.css` to test vault on each dev rebuild
---
## [prod] v0.1.5 — 17 May 2026
### New features

View file

@ -108,10 +108,11 @@ Accessible via the ribbon icon or `Ctrl+P → Pseudonymization: open panel`.
| Tab | Content |
|---|---|
| **Mappings** | Active rules · Edit · Delete · Add · Scan file · **Exceptions section** |
| **Mappings** | Active rules · Edit · Delete · Add · Scan file · Exceptions section |
| **Dictionaries** | Mini cards · Dictionary scan · Local import |
| **Exports** | Pseudonymize and export · Export correspondence table · **Export as VTT** |
| **Exports** | Create pseudonymized version · Export correspondence table · Re-export as VTT/SRT/CHAT |
| **NER** | Visible if NER enabled · Identify candidates · Confidence threshold · Function words |
| **Corpus** | Files by class · Class management · Move files · **Final export destination** |
### Highlighting and markers
@ -122,7 +123,7 @@ Highlighting is active in all open files, including `.pseudonymized.*` export fi
| 🟠 Orange + outline | Source term still present — to be pseudonymized |
| 🟢 Green + underline | Pseudonym applied directly in the file |
| 🔵 Blue + outline | NER candidate — no rule yet |
| 🔴 Red + underline | **Exception**occurrence explicitly ignored during scan (case-sensitive; persisted in mapping) |
| 🔴 Red + underline | **Exception**specific occurrence explicitly ignored (context-aware; persisted in mapping) |
In exported files, pseudonyms are wrapped in `{{Pierre}}` markers to distinguish them from raw data (enabled by default, configurable in settings).
@ -212,7 +213,7 @@ src/
| 7 — Coulmont | ✅ | Equivalent first name suggestions · JSON/CSV import |
| 8 — Side panel | ✅ | 3 tabs · Embedded NER · Wizard · Cancellation · Export highlighting |
| 9 — Structured dictionaries | ✅ | Format v1.1 · DictionaryLoader · Dictionary scan · Review modal · French communes |
| 10 — Refinement & noScribe | 🔄 | i18n · Corpus org · noScribe HTML/VTT import · per-occurrence scan · exceptions · VTT re-export |
| 10 — Refinement & noScribe | 🔄 | i18n · Corpus UI · noScribe import · per-occurrence scan · exceptions · rename cascade · export destination |
| 11 — EMCA functions | ⏳ | Turn navigation · Jefferson/ICOR correction · ELAN export |
See [ROADMAP.md](ROADMAP.md) for the full phase breakdown and planned features.

View file

@ -16,7 +16,7 @@ Architectural decision (May 2026): **identifying entity detection** is handled b
✅ Phases 08 Parsers · Engine · UI · Scopes · Highlighting · Validation · Coulmont · Panel · NER · Wizard
✅ Phase 9 Structured dictionaries · DictionaryLoader · Review modals · French communes
🔄 Phase 10 Refinement & noScribe integration (v0.1.x → v0.2.0)
✅ i18n · Corpus org · noScribe HTML/VTT/audio · Scan candidates · Exceptions · VTT re-export
✅ i18n · Corpus UI · noScribe import · scan candidats · exceptions · rename cascade · exports
⏳ Test coverage · Jefferson/ICOR checker · Audio redaction · Timestamp UI
⏳ Phase 11 Interactional analysis functions (v1.0.0)
```
@ -223,7 +223,11 @@ Goal: consolidate all existing features and add the EMCA-specific functions that
- [x] **Mappings tab grouped by scope** — File / Folder / Vault sections with active-file filter
- [x] **Status labels clarified** — "Active / Partial / Ignored / Suggested" instead of ✓/◑/✗/?; `getValidatedFor()` includes `partial`
- [x] **Scan modal — per-occurrence candidates**`MappingScanReviewModal` count column opens `OccurrencesContextModal` (✓/✗/⚠ per occurrence, "Save exceptions" button)
- [x] **Exceptions**`IgnoredOccurrence` on `MappingRule`, persisted in mapping.json, red highlighting (`pseudobs-exception`, case-sensitive, priority 0), Exceptions section in Mappings tab
- [x] **Exceptions**`IgnoredOccurrence` on `MappingRule`, persisted in mapping.json, red highlighting (`pseudobs-exception`, context-aware position, priority 0), Exceptions section in Mappings tab
- [x] **Corpus tab** — file list by class, class management, move files (cascade: .md + mapping + audio + source), final export destination (vault/next-to-source/external, optional class mirroring), `FolderSuggest` autocomplete
- [x] **Rename cascade**`vault.on('rename')` listener, `cascadeRelatedRename()` updates frontmatter (processFrontMatter), mapping scopes, words.json, exports, audio (same basename + pseudobs-audio ref), source files
- [x] **Filename warning** — banner above tabs when name contains pseudonymizable term; ✏ manual rename · ✨ neutral suggestion (transcript_N)
- [x] **Exports in original format**`markdownToSrt()`, `markdownToCha()`, `resolveExportPath()` with class mirroring, Exports tab adapts based on file type
- [ ] Unit test coverage ≥ 80% for parsers, engine, NER scanner, DictionaryLoader
- [ ] Jefferson / ICOR convention checker: hover suggestions, editor highlighting
- [ ] EMCA publication exports (PNG)

71
main.js
View file

@ -36542,6 +36542,10 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
const raw = await this.app.vault.read(found);
const data = JSON.parse(raw);
let changed = false;
if (data.scope?.type === "file" && data.scope.path === file.path) {
data.scope.path = newMdPath;
changed = true;
}
for (const rule of data.mappings ?? []) {
if (rule.scope?.type === "file" && rule.scope.path === file.path) {
rule.scope.path = newMdPath;
@ -36619,6 +36623,7 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
*/
async renameFileAndRelated(file, newBasename) {
const oldBase = file.basename;
const oldFilePath = file.path;
const folder = file.parent?.path ?? "";
const newPath = folder ? `${folder}/${newBasename}.${file.extension}` : `${newBasename}.${file.extension}`;
this._renamingRelated = true;
@ -36626,7 +36631,7 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
await this.app.vault.rename(file, newPath);
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (newFile instanceof import_obsidian14.TFile) {
await this.cascadeRelatedRename(oldBase, newBasename, newFile, file.path);
await this.cascadeRelatedRename(oldBase, newBasename, newFile, oldFilePath);
}
} finally {
this._renamingRelated = false;
@ -36667,13 +36672,11 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
const fileFolder = file.parent?.path ?? "";
if (file.extension === "md") {
try {
const content = await this.app.vault.read(file);
const updated = content.replace(
/^pseudobs-source:\s*"[^"]*"/m,
`pseudobs-source: "${newBase}.${file.extension}"`
);
if (updated !== content)
await this.app.vault.modify(file, updated);
await this.app.fileManager.processFrontMatter(file, (fm) => {
if (fm["pseudobs-source"] !== void 0) {
fm["pseudobs-source"] = `${newBase}.${file.extension}`;
}
});
} catch {
}
}
@ -36686,6 +36689,9 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
try {
const raw = await this.app.vault.read(found);
const data = JSON.parse(raw);
if (data.scope?.type === "file" && data.scope.path === oldFilePath) {
data.scope.path = file.path;
}
for (const rule of data.mappings ?? []) {
if (rule.scope?.type === "file" && rule.scope.path === oldFilePath) {
rule.scope.path = file.path;
@ -36710,10 +36716,11 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
const ef = this.app.vault.getAbstractFileByPath(newExportPath);
if (ef instanceof import_obsidian14.TFile) {
try {
const c = await this.app.vault.read(ef);
const u = c.replace(/^pseudobs-source:\s*"[^"]*"/m, `pseudobs-source: "${newBase}.${file.extension}"`);
if (u !== c)
await this.app.vault.modify(ef, u);
await this.app.fileManager.processFrontMatter(ef, (fm) => {
if (fm["pseudobs-source"] !== void 0) {
fm["pseudobs-source"] = `${newBase}.${file.extension}`;
}
});
} catch {
}
}
@ -36721,22 +36728,48 @@ var PseudObsPlugin = class extends import_obsidian14.Plugin {
}
}
const AUDIO_EXTS = ["m4a", "mp3", "wav", "ogg", "flac", "mp4", "aac", "aiff"];
const audioRenamed = /* @__PURE__ */ new Map();
for (const ext of AUDIO_EXTS) {
const audioPath = fileFolder ? `${fileFolder}/${oldBase}.${ext}` : `${oldBase}.${ext}`;
const audioFile = this.app.vault.getAbstractFileByPath(audioPath);
if (!(audioFile instanceof import_obsidian14.TFile))
continue;
const oldAudioName = audioFile.name;
const newAudioName = `${newBase}.${ext}`;
const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName;
await this.app.vault.rename(audioFile, newAudioPath);
if (file.extension === "md") {
try {
const c = await this.app.vault.read(file);
const u = c.replace(/^pseudobs-audio:\s*"[^"]*"/m, `pseudobs-audio: "${newAudioName}"`);
if (u !== c)
await this.app.vault.modify(file, u);
} catch {
audioRenamed.set(oldAudioName, newAudioName);
}
if (file.extension === "md") {
try {
const mdContent = await this.app.vault.read(file);
const audioMatch = /^pseudobs-audio:\s*"([^"]+)"/m.exec(mdContent);
if (audioMatch) {
const oldAudioName = audioMatch[1];
if (!audioRenamed.has(oldAudioName)) {
const oldAudioPath = fileFolder ? `${fileFolder}/${oldAudioName}` : oldAudioName;
const audioFile = this.app.vault.getAbstractFileByPath(oldAudioPath);
if (audioFile instanceof import_obsidian14.TFile) {
const ext = oldAudioName.split(".").pop() ?? "";
const newAudioName = ext ? `${newBase}.${ext}` : newBase;
const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName;
await this.app.vault.rename(audioFile, newAudioPath);
audioRenamed.set(oldAudioName, newAudioName);
}
}
}
} catch {
}
}
if (file.extension === "md" && audioRenamed.size > 0) {
try {
await this.app.fileManager.processFrontMatter(file, (fm) => {
const current = fm["pseudobs-audio"];
if (current && audioRenamed.has(current)) {
fm["pseudobs-audio"] = audioRenamed.get(current);
}
});
} catch {
}
}
const SOURCE_EXTS = ["srt", "vtt", "cha", "chat", "html", "txt", "yml", "yaml"];

View file

@ -1,7 +1,7 @@
{
"id": "pseudonymizer-tool",
"name": "Pseudonymizer Tool",
"version": "0.1.5",
"version": "0.1.6",
"minAppVersion": "1.7.2",
"description": "Pseudonymize and correct interactional transcripts (Jefferson, ICOR, SRT, CHAT/CHA). Designed for qualitative researchers in linguistics and conversation analysis.",
"author": "Axelle Abbadie",

View file

@ -1,6 +1,6 @@
{
"name": "pseudonymizer-tool",
"version": "0.1.5",
"version": "0.1.6",
"description": "Obsidian plugin for pseudonymizing and correcting interactional transcripts (Jefferson, ICOR, SRT, CHAT/CHA).",
"author": "Axelle Abbadie",
"homepage": "https://cv.hal.science/axelle-abbadie/",

View file

@ -470,12 +470,16 @@ export default class PseudObsPlugin extends Plugin {
if (found) {
const newPath = `${mappingTargetDir}/${base}${suffix}`;
if (found.path !== newPath) {
// Mettre à jour le scope.path des règles fichier après déplacement
// Mettre à jour le scope.path après déplacement (store + règles individuelles)
if (suffix === '.mapping.json') {
try {
const raw = await this.app.vault.read(found);
const data = JSON.parse(raw);
let changed = false;
if (data.scope?.type === 'file' && data.scope.path === file.path) {
data.scope.path = newMdPath;
changed = true;
}
for (const rule of data.mappings ?? []) {
if (rule.scope?.type === 'file' && rule.scope.path === file.path) {
rule.scope.path = newMdPath;
@ -567,6 +571,7 @@ export default class PseudObsPlugin extends Plugin {
*/
async renameFileAndRelated(file: TFile, newBasename: string): Promise<void> {
const oldBase = file.basename;
const oldFilePath = file.path; // capturer AVANT le rename (file.path change après)
const folder = file.parent?.path ?? '';
const newPath = folder ? `${folder}/${newBasename}.${file.extension}` : `${newBasename}.${file.extension}`;
@ -575,7 +580,7 @@ export default class PseudObsPlugin extends Plugin {
await this.app.vault.rename(file, newPath);
const newFile = this.app.vault.getAbstractFileByPath(newPath);
if (newFile instanceof TFile) {
await this.cascadeRelatedRename(oldBase, newBasename, newFile, file.path);
await this.cascadeRelatedRename(oldBase, newBasename, newFile, oldFilePath);
}
} finally {
this._renamingRelated = false;
@ -623,15 +628,14 @@ export default class PseudObsPlugin extends Plugin {
const s = this.settings;
const fileFolder = file.parent?.path ?? '';
// 1. Mettre à jour le frontmatter pseudobs-source dans le fichier renommé
// 1. Mettre à jour pseudobs-source dans le frontmatter via l'API Obsidian
if (file.extension === 'md') {
try {
const content = await this.app.vault.read(file);
const updated = content.replace(
/^pseudobs-source:\s*"[^"]*"/m,
`pseudobs-source: "${newBase}.${file.extension}"`
);
if (updated !== content) await this.app.vault.modify(file, updated);
await this.app.fileManager.processFrontMatter(file, (fm) => {
if (fm['pseudobs-source'] !== undefined) {
fm['pseudobs-source'] = `${newBase}.${file.extension}`;
}
});
} catch { /* ignore */ }
}
@ -644,6 +648,11 @@ export default class PseudObsPlugin extends Plugin {
try {
const raw = await this.app.vault.read(found);
const data = JSON.parse(raw);
// Scope de niveau store (chemin du fichier de transcription)
if (data.scope?.type === 'file' && data.scope.path === oldFilePath) {
data.scope.path = file.path;
}
// Scopes individuels des règles
for (const rule of data.mappings ?? []) {
if (rule.scope?.type === 'file' && rule.scope.path === oldFilePath) {
rule.scope.path = file.path;
@ -667,32 +676,68 @@ export default class PseudObsPlugin extends Plugin {
const ef = this.app.vault.getAbstractFileByPath(newExportPath);
if (ef instanceof TFile) {
try {
const c = await this.app.vault.read(ef);
const u = c.replace(/^pseudobs-source:\s*"[^"]*"/m, `pseudobs-source: "${newBase}.${file.extension}"`);
if (u !== c) await this.app.vault.modify(ef, u);
await this.app.fileManager.processFrontMatter(ef, (fm) => {
if (fm['pseudobs-source'] !== undefined) {
fm['pseudobs-source'] = `${newBase}.${file.extension}`;
}
});
} catch { /* ignore */ }
}
}
}
}
// 4. Fichiers audio avec le même basename dans le même dossier
// 4. Fichiers audio — deux stratégies :
// a) Même basename (entretien_01.m4a avec entretien_01.md)
// b) Référencé dans pseudobs-audio du frontmatter (audio noScribe à nom différent)
const AUDIO_EXTS = ['m4a', 'mp3', 'wav', 'ogg', 'flac', 'mp4', 'aac', 'aiff'];
const audioRenamed = new Map<string, string>(); // oldName → newName
// Stratégie a : même basename
for (const ext of AUDIO_EXTS) {
const audioPath = fileFolder ? `${fileFolder}/${oldBase}.${ext}` : `${oldBase}.${ext}`;
const audioFile = this.app.vault.getAbstractFileByPath(audioPath);
if (!(audioFile instanceof TFile)) continue;
const oldAudioName = audioFile.name; // capturer AVANT le rename (TFile muté in-place)
const newAudioName = `${newBase}.${ext}`;
const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName;
await this.app.vault.rename(audioFile, newAudioPath);
// Mettre à jour pseudobs-audio dans le frontmatter du .md
if (file.extension === 'md') {
try {
const c = await this.app.vault.read(file);
const u = c.replace(/^pseudobs-audio:\s*"[^"]*"/m, `pseudobs-audio: "${newAudioName}"`);
if (u !== c) await this.app.vault.modify(file, u);
} catch { /* ignore */ }
}
audioRenamed.set(oldAudioName, newAudioName);
}
// Stratégie b : pseudobs-audio dans le frontmatter (basename différent)
if (file.extension === 'md') {
try {
const mdContent = await this.app.vault.read(file);
const audioMatch = /^pseudobs-audio:\s*"([^"]+)"/m.exec(mdContent);
if (audioMatch) {
const oldAudioName = audioMatch[1];
if (!audioRenamed.has(oldAudioName)) {
// Fichier audio à renommer avec le même basename que la nouvelle transcription
const oldAudioPath = fileFolder ? `${fileFolder}/${oldAudioName}` : oldAudioName;
const audioFile = this.app.vault.getAbstractFileByPath(oldAudioPath);
if (audioFile instanceof TFile) {
const ext = oldAudioName.split('.').pop() ?? '';
const newAudioName = ext ? `${newBase}.${ext}` : newBase;
const newAudioPath = fileFolder ? `${fileFolder}/${newAudioName}` : newAudioName;
await this.app.vault.rename(audioFile, newAudioPath);
audioRenamed.set(oldAudioName, newAudioName);
}
}
}
} catch { /* ignore */ }
}
// Mettre à jour pseudobs-audio dans le frontmatter via l'API Obsidian
if (file.extension === 'md' && audioRenamed.size > 0) {
try {
await this.app.fileManager.processFrontMatter(file, (fm) => {
const current = fm['pseudobs-audio'];
if (current && audioRenamed.has(current)) {
fm['pseudobs-audio'] = audioRenamed.get(current);
}
});
} catch { /* ignore */ }
}
// 5. Fichier source originale (même basename, extension différente — .srt, .vtt, .cha, .html, .yml…)

View file

@ -10,5 +10,6 @@
"0.1.2": "1.7.2",
"0.1.3": "1.7.2",
"0.1.4": "1.7.2",
"0.1.5": "1.7.2"
"0.1.5": "1.7.2",
"0.1.6": "1.7.2"
}