Thanks a lot for your suggestions!
I ran a manual test by replacing the Markdown content in my script with this string
du texte avec des accents : é, è, à, ç, ê, î.
✅ The result was displayed correctly in Notion — all accented characters showed up as expected.
This confirms the issue is not related to UTF-8 encoding, the Notion API, or the way blocks are built in my script. The problem might come from a specific file or a font rendering issue in some cases.
I’ll dig deeper into the original resume.md
that caused the issue and report back if I find something unusual.
Thanks again for your help!
I test the .md
`wilonweb@MSI MINGW64 ~/Documents/VisualStudioCode/YT-GPT-Notion/YoutubeTranscription/mes-transcriptions/ya_juste_6_concepts_pour_tout_comprendre_au_devops (master)
$ file -i resume.md
resume.md: text/plain; charset=utf-8`
And this is my script that build the markdown
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const { Command } = require('commander');
const chalk = require('chalk');
const cliProgress = require('cli-progress');
// 🌱 Charge le .env depuis la racine du projet
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
const program = new Command();
program
.option('-m, --model <model>', 'Modèle OpenAI', 'gpt-3.5-turbo')
.option('-t, --temp <temperature>', 'Température', parseFloat, 0.5)
.option('--delay <ms>', 'Délai entre appels API (ms)', parseInt, 2000)
.option('-i, --input <path>', 'Chemin du dossier input', './input');
program.parse(process.argv);
const options = program.opts();
const inputFolder = path.resolve(options.input); // ⬅️ résout vers chemin absolu
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
console.error(chalk.red('❌ Clé API manquante dans .env'));
process.exit(1);
}
console.log(chalk.blue(`📥 Traitement du dossier : ${inputFolder}`));
const wait = ms => new Promise(res => setTimeout(res, ms));
// 🔎 Liste les fichiers de chapitres valides
function getChapterFiles() {
const files = fs.readdirSync(inputFolder)
.filter(name =>
/^\d{2}/.test(name) &&
name.endsWith('.txt') &&
!name.toLowerCase().includes('original') &&
!name.toLowerCase().includes('info')
)
.sort();
if (files.length === 0) {
console.error(chalk.red('❌ Aucun fichier de chapitre trouvé (ex: 01_intro.txt)'));
process.exit(1);
}
return files.map(filename => ({
filename,
filepath: path.join(inputFolder, filename),
title: filename.replace(/^\d+[-_]?/, '').replace(/\.txt$/, '').replace(/[_\-]/g, ' ').trim()
}));
}
// 🔗 Lecture des infos du fichier info.txt
function readInfoTxt() {
const infoPath = path.join(inputFolder, "info.txt");
if (!fs.existsSync(infoPath)) {
console.warn(chalk.yellow('⚠️ Aucun info.txt trouvé dans le dossier de transcription.'));
return {};
}
const content = fs.readFileSync(infoPath, "utf8");
const getLineValue = (label) => {
const regex = new RegExp(`^${label} ?: (.+)$`, "m");
const match = content.match(regex);
return match ? match[1].trim() : null;
};
return {
videoUrl: getLineValue("🎬 URL de la vidéo"),
channelName: getLineValue("📺 Chaîne"),
channelLink: getLineValue("🔗 Lien"),
description: content.split("## Description")[1]?.trim() || "",
raw: content
};
}
// 🔧 Récupère le nom du dossier à partir du fichier original_*.txt
function getSlugFromOriginalFile() {
const file = fs.readdirSync(inputFolder).find(f => f.startsWith("original_") && f.endsWith(".txt"));
if (!file) return "no-title-found";
return file.replace(/^original_/, "").replace(/\.txt$/, "");
}
// 🧠 Résume un texte avec OpenAI
async function summarize(text, promptTitle) {
const prompt = `${promptTitle}\n\n${text}`;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
const res = await axios.post('https://api.openai.com/v1/chat/completions', {
model: options.model,
messages: [{ role: 'user', content: prompt }],
temperature: options.temp
}, {
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
return res.data.choices[0].message.content.trim();
} catch (err) {
if (err.response?.status === 429) {
console.warn(chalk.yellow(`⚠️ ${attempt} - Limite atteinte, pause...`));
await wait(3000);
} else {
console.error(chalk.red(`❌ Erreur : ${err.message}`));
return '❌ Erreur de résumé';
}
}
}
return '❌ Résumé impossible après 3 tentatives.';
}
// MAIN
(async () => {
const chapters = getChapterFiles();
const info = readInfoTxt();
const slug = getSlugFromOriginalFile();
const title = slug.replace(/[_\-]/g, ' ').trim();
//const outputDir = path.join('output', slug);
const outputDir = path.join(__dirname, '..', 'YoutubeTranscription', 'mes-transcriptions', slug); // ✅ bon dossier
const outputFile = path.join(outputDir, 'resume.md');
fs.mkdirSync(outputDir, { recursive: true });
console.log(chalk.yellow(`📚 ${chapters.length} chapitres détectés`));
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
bar.start(chapters.length, 0);
const chapterSummaries = [];
for (const chapter of chapters) {
const text = fs.readFileSync(chapter.filepath, 'utf8').trim();
const summary = await summarize(text, `Tu es un professeur francophone. Résume en **langue française uniquement**, avec un ton structuré et pédagogique, le chapitre suivant intitulé : "${chapter.title}".`);
chapterSummaries.push({ ...chapter, summary });
bar.increment();
await wait(options.delay);
}
bar.stop();
console.log(chalk.blue('🧠 Génération du résumé global...'));
const fullText = chapterSummaries.map(c => c.summary).join('\n\n');
const globalSummary = await summarize(fullText, "Tu es un professeur francophone. Fusionne exclusivement en **langue française**, de façon concise, structurée et pédagogique, les résumés suivants :");
// 📝 Création du fichier résumé markdown
const header = `# ${title}\n\n` +
(info.videoUrl ? `🎬 [Vidéo YouTube](${info.videoUrl})\n` : '') +
(info.channelName && info.channelLink ? `📺 ${info.channelName} – [Chaîne](${info.channelLink})\n` : '') +
(info.description ? `\n## Description\n\n${info.description}\n` : '') +
`\n## Résumé global\n\n${globalSummary}\n\n` +
`## Table des matières\n` +
chapterSummaries.map((c, i) => `### Chapitre ${i + 1}: ${c.title}`).join('\n') +
'\n\n' +
chapterSummaries.map((c, i) => `### Chapitre ${i + 1}: ${c.title}\n${c.summary}\n`).join('\n');
fs.writeFileSync(outputFile, header, 'utf8');
console.log(chalk.green(`✅ Résumé structuré enregistré dans : ${outputFile}`));
// 🗂️ Copie du fichier info.txt vers le dossier output
const infoSourcePath = path.join(inputFolder, 'info.txt');
const infoDestPath = path.join(outputDir, 'info.txt');
console.log(`📦 Copie de info.txt depuis : ${infoSourcePath}`);
if (fs.existsSync(infoSourcePath)) {
fs.copyFileSync(infoSourcePath, infoDestPath);
console.log(chalk.green(`📄 info.txt copié dans : ${infoDestPath}`));
} else {
console.warn(chalk.yellow('⚠️ Aucun fichier info.txt trouvé à copier.'));
}
})();