A place to cache linked articles (think custom and personal wayback machine)
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

index.html 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang="fr">
  4. <!-- Has to be within the first 1024 bytes, hence before the `title` element
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset="utf-8">
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name="viewport" content="width=device-width,initial-scale=1">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Expérimentations GPTiennes: assistant vocal (archive) — David Larlet</title>
  13. <meta name="description" content="Publication mise en cache pour en conserver une trace.">
  14. <!-- That good ol' feed, subscribe :). -->
  15. <link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
  16. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  17. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
  18. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
  19. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
  20. <link rel="manifest" href="/static/david/icons2/site.webmanifest">
  21. <link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
  22. <link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
  23. <meta name="msapplication-TileColor" content="#f7f7f7">
  24. <meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
  25. <meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
  26. <meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
  27. <!-- Is that even respected? Retrospectively? What a shAItshow…
  28. https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
  29. <meta name="robots" content="noai, noimageai">
  30. <!-- Documented, feel free to shoot an email. -->
  31. <link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
  32. <!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
  33. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  34. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  35. <link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
  36. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  37. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  38. <link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
  39. <script>
  40. function toggleTheme(themeName) {
  41. document.documentElement.classList.toggle(
  42. 'forced-dark',
  43. themeName === 'dark'
  44. )
  45. document.documentElement.classList.toggle(
  46. 'forced-light',
  47. themeName === 'light'
  48. )
  49. }
  50. const selectedTheme = localStorage.getItem('theme')
  51. if (selectedTheme !== 'undefined') {
  52. toggleTheme(selectedTheme)
  53. }
  54. </script>
  55. <meta name="robots" content="noindex, nofollow">
  56. <meta content="origin-when-cross-origin" name="referrer">
  57. <!-- Canonical URL for SEO purposes -->
  58. <link rel="canonical" href="http://dataholic.ca/2023/04/05/gpt-assistant-vocal/">
  59. <body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">
  60. <article>
  61. <header>
  62. <h1>Expérimentations GPTiennes: assistant vocal</h1>
  63. </header>
  64. <nav>
  65. <p class="center">
  66. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  67. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  68. </svg> Accueil</a> •
  69. <a href="http://dataholic.ca/2023/04/05/gpt-assistant-vocal/" title="Lien vers le contenu original">Source originale</a>
  70. </p>
  71. </nav>
  72. <hr>
  73. <p>Dernière exploration avec GPT: est-il possible d’interfacer un <a href="https://fr.wikipedia.org/wiki/Mod%C3%A8le_de_langage">modèle de langage (LLM)</a> avec des outils logiciels existants, par exemple pour envoyer des courriels? Et d’ailleurs pourquoi?</p>
  74. <p>Démontrer <em>ad nauseam</em> que les connaissances générales de GPT ne sont pas si bonnes ou qu’il est facile de lui faire dire n’importe quoi et son contraire, tout cela fait que l’on passe à côté d’une réelle compréhension de ce genre d’outil et donc de son impact possible. Le fait que GPT fasse preuve d’une certaine “culture générale” mâtinée d’une tendance à l’affabulation est un bénéfice secondaire.</p>
  75. <p>La fonction première de ces modèles est celle d’interprétation du “langage naturel”. Cette fonction d’interprétation du langage est ce qui fait défaut aux outils informatiques depuis des lunes; barrière qui, une fois éliminée, permettrait de s’affranchir du symbolisme actuellement nécessaire et représenté par des interfaces d’utilisation contraignantes.</p>
  76. <p>Sauf que pour être en mesure de s’affranchir réellement de cette barrière, il faut que les LLM soient capables de faire le pont: comprendre d’un côté le langage humain et être capable de l’autre côté d’utiliser du langage “machine”, suivant un certain formalisme, pour transformer le verbe en action (informatique).</p>
  77. <p>GPT démontre d’ores et déjà cette capacité: la version Copilot qui permet de générer du code est en exemple. L’intégration avec Bing pour faire un moteur de recherche assisté en est une autre. Toutefois, je voulais tester moi-même comment cela pourrait fonctionner. Mon précédent test sur le code de sécurité routière (billet <a href="/2023/02/19/apprendre-a-gpt3/">1</a> et <a href="/2023/03/11/addendum-gpt/">2</a>) visait à tester la capacité de traitement et d’interprétation de GPT sur des volumes d’information supérieurs à sa fenêtre de contexte, ici, je cherche à évaluer la capacité du modèle de langage à jouer le rôle d’interface d’interprétation humain-machine.</p>
  78. <h2 id="commande-vocale-pour-courriel">Commande vocale pour courriel</h2>
  79. <p>Mon défi: était-il possible de passer une commande vocale instruisant Gmail d’envoyer un courriel?</p>
  80. <p>Les blocs Lego utilisés pour l’occasion:</p>
  81. <ul>
  82. <li>Une interface me permettant d’envoyer des messages vocaux, de récupérer ces messages vocaux dans un script de ma conception (via une <a href="https://fr.wikipedia.org/wiki/Interface_de_programmation">API</a>) et de renvoyer des réponses écrites à l’utilisateur. J’étais parti pour utiliser Discord, mais ça ne marchait pas à mon goût. En donnant mes contraintes à ChatGPT, il m’a conseillé <a href="https://telegram.org/">Telegram</a> qui s’est avéré effectivement un très bon choix.</li>
  83. <li>Un outil parole-vers-texte, là aussi pouvant être appelé par script/API, en l’occurrence le module <a href="https://platform.openai.com/docs/guides/speech-to-text">Whisper API</a> d’OpenAI</li>
  84. <li>Évidemment GPT et Gmail, les deux offrant là aussi des API pour être contrôlés par un script.</li>
  85. </ul>
  86. <p>Je m’étais fixé un objectif supplémentaire: avoir un mécanisme modulaire qui serait capable de recevoir d’autres commandes de manière flexible: par exemple, créer des événements dans un agenda, gérer des tâches, etc. J’ai donc mis en place un mécanisme de recette: un fichier de configuration définit l’ensemble des étapes et des fonctions à appeler pour réaliser une tâche particulière.</p>
  87. <p>Résultat net: un succès, avec quelques bémols. Ci-dessous une capture d’écran montrant l’échange sur l’interface web de Telegram.</p>
  88. <p>Le déclencheur de la séquence est un message vocal qui va comme suit (ceci est exactement la chaîne de caractère produite par Whisper): « Est-ce que tu peux écrire un courriel à Stéphane Guidoin pour lui dire que demain je ne rentrerai pas au travail, car il fait trop beau pour travailler. Je rentrerai après demain. Signé Robert. »</p>
  89. <p><img src="/images/2023-04-05_echange_telegram.png" alt="Échange via Telegram"></p>
  90. <p class="photoattrib">Échange avec le bot Telegram</p>
  91. <p>Pour les curieux, une section méthodologie à la fin rentre plus dans le détail (et présente quelques limites).
  92. Tout commence par un fichier de configuration qui contient les recettes. Le fichier décrit ce que chaque recette est capable de faire ainsi que les étapes pour la réaliser. Ensuite, j’ai créé un <a href="https://core.telegram.org/bots/">bot</a> Telegram, lequel est contrôlé par mon script Python.</p>
  93. <p>Lorsque l’usager envoie un message vocal au bot, le fichier son est reçu par mon script qui l’envoie à Whisper API, ce dernier générant une transcription en texte. La transcription est envoyée à GPT conjointement avec une liste contenant les noms et descriptions des recettes et une instruction: retourner le nom de la recette correspondant à la demande de l’utilisateur. Pour rendre le tout facilement utilisable par mon script Python -et c’est la clé de la démarche, je demande à GPT d’utiliser en guise de réponse le format descriptif JSON. Ça prend le format <code class="highlighter-rouge"><span class="p">{</span><span class="nt">"nom_recette"</span><span class="p">:</span><span class="w"> </span><span class="s2">"send_mail"</span><span class="p">}</span></code></p>
  94. <p>Une fois la recette sélectionnée, une confirmation est envoyée à l’utilisateur via Telegram et le script va ensuite s’en tenir à suivre les étapes de la recette, à savoir une alternance de requêtes à GPT et de fonctions auprès d’autres services, Gmail dans ce cas-ci. Les requêtes GPT sont entièrement décrites dans le fichier de configuration, les fonctions Gmail sont nommées dans le fichier de configuration, mais doivent évidemment être codées. La recette pour l’envoi de courriel ressemble à ceci:</p>
  95. <ol>
  96. <li>La requête de l’utilisateur est envoyée à GPT avec l’instruction de retourner le nom du ou des destinataires, là encore en retournant les résultats au format JSON;</li>
  97. <li>Les noms des destinataires sont envoyés à Gmail pour récupérer les adresses courriel;</li>
  98. <li>La requête de l’utilisateur est de nouveau envoyée à GPT avec l’instruction, cette fois-ci, de générer un titre et un contenu de courriel;</li>
  99. <li>Mon script produit un brouillon de courriel qui est envoyé à l’utilisateur via Telegram pour confirmation;</li>
  100. <li>Sur approbation de l’utilisateur, grâce un bouton oui/non, le courriel est envoyé.</li>
  101. </ol>
  102. <h2 id="est-ce-que-ça-marche">Est-ce que ça marche?</h2>
  103. <p>Ça fonctionne étonnamment bien, considérant que mon code ferait surement hurler un vrai développeur. De manière générale, GPT interprète de manière fiable les requêtes. Quand on lui fournit un canevas de réponse (ici une structure JSON avec des trous à remplir), il comprend toujours comment faire. Sur des dizaines d’essai, il a toujours bien procédé. Tel qu’expliqué dans la méthodologie, il a juste fallu que je gère les excès verbomoteurs de GPT.</p>
  104. <p>Je dois dire que Whisper API m’a aussi impressionné pour la transcription: à peu près pas d’erreur, il ôte les onomatopées diverses et variées et autres hésitations et arrive même à bien épelé la majorité des noms de famille.</p>
  105. <p>Mon produit est loin d’être « production ready », mais les quelques heures que j’ai passé dessus m’ont confirmé ce dont j’avais l’impression: la capacité de GPT à interpréter les demandes fait des LLM un candidat vraiment sérieux pour servir d’interface flexible. Vous me direz que Siri, Alexa et autres font déjà cela. C’est en partie vrai: Siri et Alexa font plus d’erreurs (à mes yeux) et surtout ce sont des systèmes pour lesquels il est plus difficile de s’intégrer. Ici, il est possible de faire des intégrations multiples et jusqu’à un certain point de contrôler ces intégrations. Nombre de plateformes proposent d’ores et déjà des fonctionnalités “AI-improved” et cela va surement exploser dans les prochains mois.</p>
  106. <p>Évidemment, reste la question de la réelle fiabilité de la chose. C’est à travers des intégrations à grand volume qu’il sera possible d’évaluer réellement si la fiabilité est de l’ordre de 99% ou de 90%, la différence entre un bidule perçu comme fiable ou pas fiable.</p>
  107. <p>Dernier commentaire de fond: jusqu’à un certain point, en expliquant les règles du jeu à GPT, il serait capable de générer des recettes. En lui fournissant comme exemple ma recette, je lui ai demandé de faire de même pour créer une tâche Asana; il m’a fourni une réponse qui se tenait. De la même manière, ici je me limite à envoyer un courriel à partir de zéro, mais il serait possible de répondre à un courriel. De manière plus générale, la même approche pourrait être utilisée pour faire une synthèse des courriels d’une journée, faire ressortir les courriels qui semblent nécessiter une action urgente et y répondre, etc.</p>
  108. <p>Tel que mentionné, le principal point où GPT manquait de constance et de prévisibilité pour servir de pont humain-machine est cette tendance à être inutilement verbeux et à fournir une réponse du type</p>
  109. <p><code class="highlighter-rouge">Voici la structure JSON répondant à votre requête:
  110. {"recette": "send_mail"}</code></p>
  111. <p>Alors que l’on voudrait simplement la structure JSON. J’ai contourné le problème avec une expression régulière, mais c’est… bof bof. L’exemple de Copilot montre toutefois que lorsqu’entrainé dans cet objectif, un LLM est capable de s’en tenir à des formats structurés.</p>
  112. <p>L’autre enjeu dans ce cas d’usage est la manière d’épeler les noms de famille. À ma surprise, Whisper avait la majorité des noms de famille correctement. Mais quand il les manquait, je n’ai pas trouvé de manière fiable de faire comprendre à GPT que si je lui donnais une série de lettres après le nom de famille, ça disait comme épeler le nom. Par ailleurs, l’API de Gmail n’est pas très tolérante aux fautes d’orthographe quand on cherche un nom, donc récupérer une adresse courriel avec une erreur dans le nom ne marche pas. C’est la principale limite, insurmontée à ce stade, dans ma démarche.</p>
  113. <p>Whisper API supporte uniquement des messages d’une minute. Il existe évidemment des approches pour segmenter un fichier audio et le transcrire en plusieurs morceaux, toutefois je n’ai pas implémenté cette fonction. Mes tests se sont donc limités sur des messages vocaux de moins d’une minute. Quoiqu’il en soit, dans la majorité de mes tests, GPT a suivi les consignes; que je lui demande un courriel court ou plus long, formel ou informel, tutoiement ou vouvoiement et autres permutations que j’ai tentées. La génération du titre du courriel laissait parfois à désirer, mais c’est mieux que beaucoup de titre de courriel que nous nous envoyons quotidiennement (quand il y a un titre…). Genre de petite limitation un peu dommage: GPT n’interprétait pas que quand je lui disais que le message allait à ma conjointe, il pouvait automatiquement sélectionner une formulation informelle et le tutoiement.</p>
  114. <p>Je n’ai pas mis en place beaucoup de chemins alternatifs: si l’adresse courriel n’est pas trouvée, si l’utilisateur veut ajuster le brouillon, etc. Ça se ferait parfaitement, ça prenait du temps dont je ne disposais plus.</p>
  115. <p>Tout cela est accompli avec environ 300 lignes de script Python et un fichier de configuration JSON d’une centaine de lignes. Je demeure impressionné par la facilité de mise en œuvre. Les deux tâches qui m’ont pris le plus de temps: corriger mon installation de Homebrew qui n’avait pas appréciée de passer sur une puce M1 et gérer les <em>callbacks</em> de l’API de Telegram. Le contrôle de Telegram se fait avec la librairie <a href="https://pypi.org/project/pyTelegramBotAPI/">Telebot</a>, tandis que pour Whisper, GPT et Gmail, j’utilise les librairies officielles. Le modèle utilisé pour GPT est <code class="highlighter-rouge">gpt-3.5-turbo</code>, je n’ai pas encore accès à GPT4 via l’API.</p>
  116. </article>
  117. <hr>
  118. <footer>
  119. <p>
  120. <a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
  121. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
  122. </svg> Accueil</a> •
  123. <a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
  124. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
  125. </svg> Suivre</a> •
  126. <a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
  127. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
  128. </svg> Pro</a> •
  129. <a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
  130. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
  131. </svg> Email</a> •
  132. <abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
  133. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
  134. </svg> Légal</abbr>
  135. </p>
  136. <template id="theme-selector">
  137. <form>
  138. <fieldset>
  139. <legend><svg class="icon icon-brightness-contrast">
  140. <use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
  141. </svg> Thème</legend>
  142. <label>
  143. <input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
  144. </label>
  145. <label>
  146. <input type="radio" value="dark" name="chosen-color-scheme"> Foncé
  147. </label>
  148. <label>
  149. <input type="radio" value="light" name="chosen-color-scheme"> Clair
  150. </label>
  151. </fieldset>
  152. </form>
  153. </template>
  154. </footer>
  155. <script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
  156. <script>
  157. function loadThemeForm(templateName) {
  158. const themeSelectorTemplate = document.querySelector(templateName)
  159. const form = themeSelectorTemplate.content.firstElementChild
  160. themeSelectorTemplate.replaceWith(form)
  161. form.addEventListener('change', (e) => {
  162. const chosenColorScheme = e.target.value
  163. localStorage.setItem('theme', chosenColorScheme)
  164. toggleTheme(chosenColorScheme)
  165. })
  166. const selectedTheme = localStorage.getItem('theme')
  167. if (selectedTheme && selectedTheme !== 'undefined') {
  168. form.querySelector(`[value="${selectedTheme}"]`).checked = true
  169. }
  170. }
  171. const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
  172. window.addEventListener('load', () => {
  173. let hasDarkRules = false
  174. for (const styleSheet of Array.from(document.styleSheets)) {
  175. let mediaRules = []
  176. for (const cssRule of styleSheet.cssRules) {
  177. if (cssRule.type !== CSSRule.MEDIA_RULE) {
  178. continue
  179. }
  180. // WARNING: Safari does not have/supports `conditionText`.
  181. if (cssRule.conditionText) {
  182. if (cssRule.conditionText !== prefersColorSchemeDark) {
  183. continue
  184. }
  185. } else {
  186. if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
  187. continue
  188. }
  189. }
  190. mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
  191. }
  192. // WARNING: do not try to insert a Rule to a styleSheet you are
  193. // currently iterating on, otherwise the browser will be stuck
  194. // in a infinite loop…
  195. for (const mediaRule of mediaRules) {
  196. styleSheet.insertRule(mediaRule.cssText)
  197. hasDarkRules = true
  198. }
  199. }
  200. if (hasDarkRules) {
  201. loadThemeForm('#theme-selector')
  202. }
  203. })
  204. </script>
  205. </body>
  206. </html>