79705044

Date: 2025-07-17 15:26:54
Score: 0.5
Natty:
Report link

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.'));
  }
})();
Reasons:
  • Blacklisted phrase (0.5): Thanks
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (1):
Posted by: WilOnWeb