Utilisateur:Thomas : Différence entre versions

De Design numérique
Aller à : navigation, rechercher
m
m (Affichage v2)
 
(118 révisions intermédiaires par le même utilisateur non affichées)
Ligne 526 : Ligne 526 :
 
que l'on puisse comparer le même texte chamboulé par son scan près d'autres langues.
 
que l'on puisse comparer le même texte chamboulé par son scan près d'autres langues.
  
J'ai trouvé la police Noto (qui est par défaut sur Debian 10 en fait) de Google, une linéale qui possède tous les alphabets du monde paraît-il. Elle est disponible dans plein de styles différents donc c'est bien pratique.
+
J'ai trouvé la police Noto (qui est par défaut sur Debian 10 en fait) de Google, une linéale qui possède tous les alphabets du monde. Elle est disponible dans plein de styles différents.
  
=PROJET Q2=
+
===Suite du projet===
==Intention==
+
J'ai changé complètement de mise en page, pour être plus clair dans mon intention. Ce que je veux montrer c'est la comparaison entre les résultats de Tesseract sur le même texte avec différents réglages de langues.
Mon objectif est d'utiliser mon Raspberry Pi comme d'une personne qui utiliserait divers réseaux sociaux en suivant uniquement ce que les algorithmes de recommandation lui conseillent. Je veux créer principalement un compte Facebook qui me permettrait de créer les autres comptes en les liant à celui-là. Mon programme tournerait en continu sur le Raspberry et regarderait en boucle des vidéos sur Youtube, écouterait une playlist sur-mesure sur Spotify, suivrait les comptes recommandés par Twitter, etc.
+
Pour ça je conserve les deux premiers paragraphes du texte, et je re-génère avec Imagemagick et Tesseract toutes les comparaisons possibles avec l'extrait en thaïlandais.
 +
Sur chaque version du poster, il y aura le texte d'origine en thaïlandais, le texte avec lequel il sera comparé (dans une autre langue), et le résultat de Tesseract de l'interprétation du texte en thaïlandais quand les deux langues en question sont dans les paramètres.
 +
 
 +
Exemples de résultats:
 +
 
 +
[[Fichier:poster_nor.jpg|500px]]
 +
 
 +
==Affiche pour le Rideau de Perles==
 +
Pour l'ouverture du Rideau de Perles, j'ai fait un "générateur" d'affiches. Par-dessus les posters j'ai rajouté le mot "ouverture" dans toutes les 9 langues du mode d'emploi que j'ai sélectionnées.
 +
Tous ces mots en surimpression sont semi-transparents, sauf "ouverture" en français et celui dans la langue correspondant au poster en arrière-plan.
 +
 
 +
Exemples :
 +
 
 +
[[Fichier:poster_grc.jpg|500px|left]][[Fichier:poster_ara.jpg|500px|center]]
 +
 
 +
Les affiches sont générées avec PHP/HTML/CSS, et le changement d'opacité est géré par JavaScript.
 +
J'ai fait une version web qui alterne entre toutes
 +
Le résultat est visible sur le site [rideaudeperles.be], avec les autres propositions du cours (recharger la page plusieurs fois pour voir les autres versions, qui sont choisies aléatoirement à chaque chargement de la page).
 +
 
 +
[[Fichier:postergif.gif]]
 +
 
 +
==Code==
 +
 
 +
===PHP===
 +
<syntaxhighlight lang="php">
 +
<!DOCTYPE html>
 +
<html>
 +
  <head>
 +
    <meta charset="UTF-8">
 +
    <link href="affiche3_print.css" type="text/css" rel="stylesheet" media="all">
 +
    <!--<link href=affiche3_web.css type=text/css rel=stylesheet media=screen>-->
 +
 
 +
    <?php
 +
      ini_set('display_errors', 1);
 +
      ini_set('display_startup_errors', 1);
 +
      error_reporting(E_ALL);
 +
      ini_set('max_execution_time', 300);
 +
 
 +
      include 'variables.php';
 +
    ?>
 +
 
 +
  </head>
 +
 
 +
  <body>
 +
    <div class="hidden">
 +
      <div id="pur_ara" > <?php echo  htmlspecialchars($pur_ara); ?> </div>
 +
      <div id="pur_bel" > <?php echo htmlspecialchars($pur_bel); ?> </div>
 +
      <div id="pur_chi_sim" > <?php echo htmlspecialchars($pur_chi_sim); ?> </div>
 +
      <div id="pur_grc" > <?php echo htmlspecialchars($pur_grc); ?> </div>
 +
      <div id="pur_hrv" > <?php echo htmlspecialchars($pur_hrv); ?> </div>
 +
      <div id="pur_nor" > <?php echo htmlspecialchars($pur_nor); ?> </div>
 +
      <div id="pur_pol" > <?php echo htmlspecialchars($pur_pol); ?> </div>
 +
      <div id="pur_tha" > <?php echo htmlspecialchars($pur_tha); ?> </div>
 +
      <div id="pur_tur" > <?php echo htmlspecialchars($pur_tur); ?> </div>
 +
 
 +
      <div id="ara_tha" > <?php echo htmlspecialchars($ara_tha); ?> </div>
 +
      <div id="bel_tha" > <?php echo htmlspecialchars($bel_tha); ?> </div>
 +
      <div id="chi_sim_tha" > <?php echo htmlspecialchars($chi_sim_tha); ?> </div>
 +
      <div id="grc_tha" > <?php echo htmlspecialchars($grc_tha); ?> </div>
 +
      <div id="hrv_tha" > <?php echo htmlspecialchars($hrv_tha); ?> </div>
 +
      <div id="nor_tha" > <?php echo htmlspecialchars($nor_tha); ?> </div>
 +
      <div id="pol_tha" > <?php echo htmlspecialchars($pol_tha); ?> </div>
 +
      <div id="tur_tha" > <?php echo htmlspecialchars($tur_tha); ?> </div>
 +
      <div id="tha_tha" > <?php echo htmlspecialchars($tha_tha); ?> </div>
 +
    </div>
 +
 
 +
  <section class="page">
 +
    <div class ="hidden" id="rand"></div>
 +
    <section class="source" id="txt_origine">
 +
      <div class="wrapper">
 +
        <?php echo $pur_tha; ?>
 +
      </div>
 +
    </section>
 +
 
 +
    <div class="plus"><p>+</p></div>
 +
 
 +
    <section class="source" id="txt_côté">
 +
      <div class="wrapper" id='côté'></div>
 +
    </section>
 +
 
 +
    <div class="langue">
 +
      <div id="lang"></div>
 +
    </div>
 +
 
 +
    <section id="txt_ocr"></section>
 +
 
 +
    <section class="info_karaoké" id="karaoké">
 +
      <div class="opacity" id='K_ara'>افتتاح</div>
 +
      <div class="opacity" id='K_bel'>АДКРЫЦЦЁ</div>
 +
      <div class="opacity" id='K_chi_sim'>开幕</div>
 +
      <div class="opacity" id='K_grc'>ΕΓΚΑΙΝΙΑ</div>
 +
      <div class="opacity" id='K_hrv'>OTVARANJE</div>
 +
      <div class="opacity" id='K_fra'>OUVERTURE</div>
 +
      <div class="opacity" id='K_nor'>ÅdivNING</div>
 +
      <div class="opacity" id='K_pol'>OTWARCIE</div>
 +
      <div class="opacity" id='K_tha'>เปิด</div>
 +
      <div class="opacity" id='K_tur'>AÇILIŞ</div>
 +
    </section>
 +
 
 +
  </section>
 +
 
 +
  <script type=text/javascript src=gif.js></script>
 +
 
 +
  </body>
 +
 
 +
</html>
 +
 
 +
</syntaxhighlight>
 +
 
 +
===CSS===
 +
<syntaxhighlight lang="css">
 +
p, h1, h2, h3, h4, h5, h6, footer, header, section, article, aside{
 +
margin:0;
 +
padding:0;
 +
font-weight:normal;
 +
}
 +
 
 +
@media print{
 +
section.page{
 +
    overflow: hidden;
 +
margin:0;
 +
  }
 +
  head{
 +
  display: none;
 +
  }
 +
}
 +
 
 +
@page{
 +
size:297mm 420mm;
 +
margin:0;
 +
/*permet de forcer le navigateur à choisir autimatiquement le bon format pour imprimer*/
 +
}
 +
 
 +
html, body {
 +
margin:0;
 +
padding:0;
 +
font-size: 10pt;
 +
background-color: black;
 +
font-family: monospace;
 +
overflow: hidden;
 +
position: relative;
 +
}
 +
 
 +
header{
 +
height: 10mm;
 +
margin-top: 5mm;
 +
}
 +
 
 +
section.page{
 +
  background-color: white;
 +
  box-sizing: border-box;
 +
  height: 420mm;
 +
  width: 297mm;
 +
  padding: 0mm;
 +
margin-left: auto;
 +
  margin-right: auto;
 +
  white-space: normal;
 +
  overflow: hidden;
 +
  position: relative;
 +
  text-overflow: ellipsis;
 +
  }
 +
 
 +
p{
 +
width: 9%;
 +
text-align: center;
 +
font-family: 'IBM Plex Mono';
 +
position: relative;
 +
float: left;
 +
margin-right: 0.8rem;
 +
display:inline-block;
 +
}
 +
 
 +
div.ocr{
 +
position: relative;
 +
width: 100%;
 +
margin-top: 5mm;
 +
text-align: center;
 +
 
 +
}
 +
 
 +
.texte{
 +
font-size: 5.5rem;
 +
letter-spacing: -0.5rem;
 +
writing-mode: vertical-rl;
 +
text-orientation: upright;
 +
}
 +
 
 +
.langue{
 +
font-size: 3rem;
 +
transform: translate(4pt);
 +
}
 +
 
 +
#clear{
 +
clear:both;
 +
}
 +
 
 +
@font-face {
 +
font-family: 'IBMPlexMono';
 +
src: url('fonts/IBMPlexMono-SemiBold.otf');
 +
}
 +
 
 +
@font-face {
 +
font-family: 'IBMPlexArabic';
 +
src: url('fonts/IBMPlexArabic-SemiBold.otf');
 +
}
 +
 
 +
@font-face {
 +
font-family: 'IBMPlexSansThai';
 +
src: url('fonts/IBMPlexSansThai-SemiBold.otf');
 +
}
 +
 
 +
@font-face {
 +
font-family: 'Ubuntu';
 +
src: url('fonts/UbuntuMono-B.ttf');
 +
}
 +
 
 +
@font-face {
 +
font-family: 'WenQuanYi';
 +
src: url('fonts/WenQuanYi Micro Hei Mono.ttf');
 +
}
 +
 
 +
#ara{
 +
font-family: 'IBMPlexArabic';
 +
}
 +
 
 +
#tha{
 +
  font-family: 'IBMPlexSansThai';
 +
}
 +
 
 +
#grc{
 +
font-family: 'Ubuntu';
 +
}
 +
</syntaxhighlight>
 +
 
 +
 
 +
===JavaScript===
 +
<syntaxhighlight lang="javascript">
 +
var ara_tha = document.getElementById("ara_tha");
 +
ara_tha = ara_tha.textContent;
 +
var bel_tha = document.getElementById("bel_tha");
 +
bel_tha = bel_tha.textContent;
 +
var chi_sim_tha = document.getElementById("chi_sim_tha");
 +
chi_sim_tha = chi_sim_tha.textContent;
 +
var grc_tha = document.getElementById("grc_tha");
 +
grc_tha = grc_tha.textContent;
 +
var hrv_tha = document.getElementById("hrv_tha");
 +
hrv_tha = hrv_tha.textContent;
 +
var nor_tha = document.getElementById("nor_tha");
 +
nor_tha = nor_tha.textContent;
 +
var pol_tha = document.getElementById("pol_tha");
 +
pol_tha = pol_tha.textContent;
 +
var tha_tha = document.getElementById("tha_tha");
 +
tha_tha = tha_tha.textContent;
 +
var tur_tha = document.getElementById("tur_tha");
 +
tur_tha = tur_tha.textContent;
 +
 
 +
var pur_ara = document.getElementById("pur_ara");
 +
pur_ara = pur_ara.textContent;
 +
var pur_bel = document.getElementById("pur_bel");
 +
pur_bel = pur_bel.textContent;
 +
var pur_chi_sim = document.getElementById("pur_chi_sim");
 +
pur_chi_sim = pur_chi_sim.textContent;
 +
var pur_grc = document.getElementById("pur_grc");
 +
pur_grc = pur_grc.textContent;
 +
var pur_hrv = document.getElementById("pur_hrv");
 +
pur_hrv = pur_hrv.textContent;
 +
var pur_nor = document.getElementById("pur_nor");
 +
pur_nor = pur_nor.textContent;
 +
var pur_pol = document.getElementById("pur_pol");
 +
pur_pol = pur_pol.textContent;
 +
var pur_tha = document.getElementById("pur_tha");
 +
pur_tha = pur_tha.textContent;
 +
var pur_tur = document.getElementById("pur_tur");
 +
pur_tur = pur_tur.textContent;
 +
 
 +
var l_tha = [ara_tha, bel_tha, chi_sim_tha, grc_tha, hrv_tha, nor_tha, pol_tha, tha_tha, tur_tha];
 +
var l_pur = [pur_ara, pur_bel, pur_chi_sim, pur_grc, pur_hrv, pur_nor, pur_pol, pur_tha, pur_tur];
 +
var l_lg = ['ARABE', 'BIÉLORUSSE', 'CHINOIS<br>SIMPLIFIÉ', 'GREC', 'CROATE', 'NORVÉGIEN', 'POLONAIS', 'THAÏLANDAIS', 'TURC'];
 +
var langues = ['K_ara', 'K_bel', 'K_chi_sim', 'K_grc', 'K_hrv', 'K_nor', 'K_pol', 'K_tha', 'K_tur'];
 +
 
 +
window.onload = function start() {
 +
  timer();
 +
  setInterval(timer, 500);
 +
}
 +
var i = 0;
 +
 
 +
//la fonction timer() utilise un compteur qui défile externe pour synchroniser
 +
//le clignotement et le changement de texte
 +
function timer(){
 +
  i = i%9;
 +
  document.getElementById("rand").innerHTML = i;
 +
  var lang = document.getElementById(langues[i]);
 +
  var w_tha = document.getElementById("txt_ocr");
 +
  var pur = document.getElementById("côté");
 +
  var lg = document.getElementById("lang");
 +
 
 +
  lang.style.opacity = '1';
 +
  w_tha.innerHTML = l_tha[i];
 +
  pur.innerHTML = l_pur[i];
 +
  lg.innerHTML = l_lg[i];
 +
  w_tha.style.fontFamily = "NotoMed";
 +
  pur.style.fontFamily = "NotoMed";
 +
 
 +
  if (i==0){
 +
    document.getElementById(langues[8]).style.opacity = '0.5';
 +
    w_tha.style.fontFamily = "NotoAra";
 +
    pur.style.fontFamily = "NotoAra";
 +
 
 +
  } else if (i==2){
 +
    document.getElementById(langues[i-1]).style.opacity = '0.5';
 +
    w_tha.style.fontFamily = "NotoMed";
 +
    pur.style.fontFamily = "NotoMed";
 +
 
 +
  } else if (i==7){
 +
    document.getElementById(langues[i-1]).style.opacity = '0.5';
 +
    //console.log('coucou');
 +
    w_tha.style.fontFamily = "NotoThai";
 +
    pur.style.fontFamily = "NotoThai";
 +
 
 +
  } else {
 +
    document.getElementById(langues[(i-1)]).style.opacity = '0.5';
 +
  }
 +
  i++;
 +
}
 +
</syntaxhighlight>
 +
 
 +
==Possibles améliorations ?==
 +
Une possibilité serait de faire une édition en conservant le même principe, mais en compilant toutes les combinaisons possibles de langues.
 +
 
 +
=PORTRAIT-ROBOT=
 +
==Lien GitLab du projet==
 +
[https://gitlab.com/123450/portrait-robot GitLab]
 +
 
 +
==Partie concept==
 +
===Intention===
 +
Mon objectif est d'utiliser mon Raspberry Pi comme d'une personne qui utiliserait divers réseaux sociaux en suivant uniquement ce que les algorithmes de recommandation lui conseillent. Je veux créer principalement un compte Google qui me permettrait de créer les autres comptes en les liant à celui-là. Mon programme tournerait en continu sur le Raspberry et regarderait en boucle des vidéos sur Youtube, écouterait une playlist sur-mesure sur Spotify, suivrait les comptes recommandés par Twitter, etc.
 
Je veux voir ce que les différents algorithmes vont créer comme personnalité à ce faux compte.
 
Je veux voir ce que les différents algorithmes vont créer comme personnalité à ce faux compte.
 +
C'est une boucle de rétroaction crée par l'usage : l'utilisation même détermine cette utilisation.
 +
 +
===Scénarios===
 +
Pour l'instant le scénario de mon "utilisateur qui ne choisit pas" (appelons-le Philippe) est de se connecter sur un réseau social, cliquer sur le premier lien disponible et de suivre sans interruption ce que lui propose les algorithmes de recommandation pendant une durée indéfinie.
 +
C'est un comportement protocolaire qui n'est pas très réaliste, mais il évoque bien la [https://www.theatlantic.com/technology/archive/2013/07/the-machine-zone-this-is-where-you-go-when-you-just-cant-stop-looking-at-pictures-on-facebook/278185/ "zone de la machine"].
 +
 +
Sur Youtube, il clique sur le premier lien de la première ligne et continue à regarder ce qui se trouve dans la case "up-next". S'il s'arrête (quand je le débranche), la fois d'après il reprend à partir de la dernière vidéo dans le log (un fichier texte qui s'agrandit de liens au fur et à mesure).
 +
 +
Sur Spotify même principe, sauf que le bot enregistre le titre/le nom de l'artiste pour chaque morceau.
 +
 +
Sur Twitter, il clique sur la première personne recommandée dans "Who follow ?" et prend une capture d'écran et enregistre le nom de la personne en question. Il s'abonne/aime le profil pour que l'algorithme de Twitter affine ses préférences. Philippe saute de personne en personne indéfiniment.
 +
 +
Sur Facebook même principe que Twitter.
 +
 +
L'objectif maintenant c'est de trouver une narration, de fictionnaliser le comportement de Philippe pour le rendre plus plausible en tant que personne, pour être plus raccord avec mon propos.
 +
 +
Quel autre comportement Philippe peut-il avoir ?
 +
* Cliquer sur une vidéo/un article/un post au hasard parmi la première page ? Parmi la première ligne ?
 +
* Cliquer sur la vidéo la plus "visible", attirante à l'oeil (avec un titre uniquement en CAPITALE ? Le thumbnail avec le plus de contraste ?)
 +
* Regarder des vidéos/articles/posts en boucle à l'infini ?
 +
* Seulement pendant des horaires de bureau, comme si c'était un bullshit job ? Est-ce que son "emploi du temps" est séparé avec 2h de Facebook/2h de YouTube/2h d'Instagram/2h de Twitter ?
 +
* Regarder du contenu seulement pendant la nuit, pendant l'inverse des horaires de bureau -> comme pour une passion, un seul divertissement en dehors du cadre du travail ?
 +
* Le faire se comporter comme un alter ego qui est actif quand je ne suis pas actif ? (Quand je dors globalement)
 +
* Est-ce que Philippe reste une longue période sur la même plateforme avant de changer ? Quelle durée ? 1 semaine ? 1 mois ?
 +
* Est-ce que Philippe se déplace (migrations pendulaires ?) ? Est-ce qu'il peut voyager entre les pays en prenant le bus VPN ou rentrer en France pendant les vacances ?
 +
* Est-ce que Philippe change d'ordi de temps en temps et s'active sur le mien ou celui de mes colocs ?
 +
* Est-ce que Philippe passe les pubs ou bien est-ce que'il les regarde toutes assidûment (voire même clique dessus) ?
 +
 +
Réfléchir à la relation entre "l'agentivité" de l'usager, sa capacité à prendre des décisions et à agir, et la construction d'une figure abstraite complètement déterminée par l'algorithme ?
 +
 +
Apparemment malgré ces dispositifs de facilitation/suppression du choix et de l'autonomie, les utilisateurs réels sont la plupart du temps quand même en autonomie. Ils n'ont recourt que ponctuellement aux algorithmes de recommandation, parfois dans des contextes bien particuliers (quand ils écoutent de la musique en arrière-plan en faisant autre chose par exemple.
 +
(cf. article LES ALGORITHMES DE RECOMMANDATION MUSICALE ET L’AUTONOMIE DE L’AUDITEUR, Jean-Samuel Beuscart, Samuel Coavoux et Sisley Maillard)
 +
 +
L'article soulève aussi la question du goût : est-ce que ces algorithmes créent des utilisateurs dépassionnés, sans jugement de goût et sans réelles préférences ?
 +
 +
Questionner l'objectif dans l'utilisation de certains réseaux : Twitter plus politique que Youtube ou Spotify ?</br>
 +
Quel partie de la personnalité je veux construire à travers ce projet, dans quel domaine je veux que Philippe ait une opinion ?
 +
 +
 +
===Philippe ?===
 +
Est-ce que Philippe est le bon nom ? Est-ce que genrer Philippe est une bonne idée, ou bien un prénom mixte (comme Dominique) correspondrait mieux vu que l'on parle d'un bot/programme ?
 +
Liste de prénoms mixtes (qui s'écrivent pareil) d'après Wikipédia (j'ai enlevé les moins ambigus) :
 +
* Dominique
 +
* Camille
 +
* Alex
 +
* Alix
 +
* Sacha
 +
* Ange
 +
* Claude
 +
* Lou
 +
* Candide
 +
* Charlie (apparemment le mieux réparti au niveau de la parité [https://www.parents.fr/prenoms/nos-selections-de-prenoms/les-prenoms-mixtes-80014])
 +
* Yaël (bien réparti aussi)
 +
* Céleste
 +
* Andrea
 +
* Louison (lui aussi)
 +
* Mahé
 +
* Gwenn
 +
* Loan
 +
* Philippe (apparemment très rarement féminin)
 +
En clair, le problème est comment ne pas rattacher le bot à une norme de représentation.
 +
 +
Cependant le programme va tourner sur un Raspberry en Belgique, il sera situé et sa navigation en sera impactée, donc je pourrais peut-être choisir un prénom mixte en fonction des statistiques de prénoms belges ? L'idée est de partir d'une base d'utilisateur.ice lambda, dans la moyenne, pour voir ce que les algorithmes font de cette base.
 +
 +
Est-ce que le bot a même besoin d'un prénom ? La raison pour laquelle j'y tiens c'est pour l'adresse mail et tout ce qui tourne autour, et que pour moi la personnalité/l'identité s'ancrent dans le prénom.
 +
[https://www.quora.com/Why-is-naming-things-hard-in-computer-science-and-how-can-it-can-be-made-easier Pourquoi donner des noms aux choses est compliqué ?]
 +
 +
En cherchant des statistiques ethnographiques sur les catégories de personnes qui regardent YouTube, je suis tombé sur [https://www.thinkwithgoogle.com/intl/fr-fr/tendances/insights/etude-ethnographique-la-realite-des-usages-de-youtube-en-2016/ cet article de Google], qui vante les mérites de YouTube. Il nous apprend seulement que plus de la moitié des gens entre 16 et 44 ans en France vont tous les jours sur YouTube (2016). Mais il nous renseigne sur la façon dont Google veut que YouTube soit perçu : un lieu d'ouverture, de partage, de multiplicité des choix, de liberté, et donc pas du tout comme je veux le montrer : une machine qui restreint les choix et peut enfermer dans une bulle de filtre.
 +
 +
Dans les prénoms mixtes,  j'aime bien Dominique pour faire un jeu de mot avec le [https://fr.wikipedia.org/wiki/Document_Object_Model DOM] (pas super pertinent mais renvoie au web), mais le prénom est un peu vieillot (et donc ne rentre pas trop dans la catégorie 18-35 ans qui sont les plus actifs sur YouTube).
 +
 +
J'aime aussi [https://fr.wikipedia.org/wiki/Mot_%C3%A9pic%C3%A8ne Gwenn], qui sonne bien comme diminutif de pleins de prénoms possibles. Aussi, si je considère le bot comme mon espèce d'alter ego (puisqu'il me suit physiquement et est localisé dans mon appartement), le fait que ce soit un prénom d'origine bretonne colle bien.
 +
Définition d'[https://fr.wikipedia.org/wiki/Mot_%C3%A9pic%C3%A8ne épicène].
 +
 +
Mais ces prénoms sont assez localisés géographiquement/culturellement, et j'ai un peu un dilemme entre un prénom choisi pour sa neutralité ou avec des statistiques (mais rien n'est vraiment neutre), ou alors un prénom qui donne déjà une connotation/un ancrage culturel.
 +
 +
Pour l'adresse mail et pour créer un compte (nom d'utilisateur et tout), il faudrait peut-être aussi un numéro, style <gwenn58@gmail.com> ?</br>, et un nom de famille.
 +
Quel numéro peut avoir du sens ? <br/>
 +
Souvent c'est une date de naissance ou un numéro lié à l'endroit où l'on habite.
 +
 +
===Décisions===
 +
====Horaires====
 +
Je veux que le bot soit comme une personne qui travaille avec des horaires de bureaux toutes la semaine, et utilise les réseaux sociaux comme moyen principal de divertissement/comme hobby.
 +
Ses horaires de bureau seraient 8h30-12h et 13h30-17h30, du lundi au vendredi. Le bot serait actif sur la pause de midi, le soir et le weekend, soit :
 +
* de 12h30-13h30, 18h30-19h30 et 21h-23h du lundi au vendredi
 +
* 10h-12h, 13h-19h30, 21h-00h le weekend
 +
(en comptant des moments de repas + sommeil).
 +
Il faut que je me renseigne sur cron pour la planification des horaires de fonctionnement du programme.
 +
 +
====Matérialité====
 +
Le programme tournerait sur mon Raspberry Pi, le bot aurait donc une matérialité physique. Il se déplace avec moi si je change d'appartement,
 +
 +
====Localisation====
 +
Le Raspberry fonctionnerait dans mon appartement, à Bruxelles. Il dépend de ma localisation.
 +
 +
====Réseaux====
 +
Dans un premier temps le bot utiliserait uniquement YouTube (car je ne suis pas sûr d'avoir le temps de creuser le projet avec Spotify ou Twitter par exemple), mais mon envie de départ était que le bot soit multi-plateforme.
 +
 +
====Pubs====
 +
Pour l'instant le bot utilise une extension qui passe les pubs (je ne suis pas sûr de la fiabilité de [https://www.lemondeinformatique.fr/actualites/lire-en-france-36-des-internautes-utilisent-un-adblock-66597.html cet article] mais cela semble assez répandu pour que je le considère sur une personne "lambda".
 +
 +
===Le doute ?===
 +
Je me prend un peu la tête sur la définition de la personnalité du bot/sa personnalité pour le rendre humain, mais ce qui m'intéresse c'est plus les résultats que l'expérience va produire.
 +
Je vais plutôt me concentrer sur des protocoles multiples, pour voir les différents types de résultats ça peut produire. Plutôt que de me concentrer sur un choix ultime, je m'autorise à changer de compte par exemple, en changeant les paramètres du bot. Le numérique me permet une démultiplication des identités du bot, qu'il ait des alias, des forks, et que finalement c'est plus intéressant que d'imiter de manière automatique un comportement humain.
 +
Le nom du bot peut être multiple, et je cherche un nom de figure/personnage fictif, dont l'histoire incarnerait les thèmes de mon projet.
 +
 +
J'ai pensé ironiquement à un nom de [https://fr.wikipedia.org/wiki/Dilemme_corn%C3%A9lien#Personnages_de_fiction_face_%C3%A0_un_dilemme_corn%C3%A9lien personnage qui fait face à un dilemme cornélien], alors que le bot fait le moins de choix possible.
 +
 +
Pour ce premier bot j'ai choisi Cinna, et puisque la pièce de Corneille a été écrite en 1643 son compte Google sera [cinna1643@gmail.com].
 +
Ce bot tourne 24/24h, regarde les vidéos en entier et passe les pubs.
 +
 +
 +
==Mise en forme==
 +
===Vidéo===
 +
Avec les 22 premières vidéos que j'ai récupérées, je voulais faire une sorte de méga mashup  en fusionnant 1 seconde de chaque vidéo, pour faire une sorte de parcours condensé du parcours du bot.
 +
Pour cela j'ai utilisé ffmpeg, qui permet de faire des manipulations vidéo sur plein de fichiers en même temps (comme ImageMagick pour les images par exemple) en ligne de commande.
 +
J'ai essayé de faire un script bash qui permet de couper chaque vidéo pour garder 1 seconde, et ensuite de fusionner toutes ces vidéos entre elles. Je pense que j'ai des problèmes de codecs puisque l'audio est décalé et qu'à un moment l'écran devient complètement vert. Toutes les vidéos YouTube ne sont pas dans le même format étonnamment, je pense que c'est une question de réglages. Avec ffmpeg on peut aussi faire des filtres pour convertir d'un coup plusieurs vidéos dans le même codec mais ça m'a l'air compliqué, je vais creuser.
 +
 +
J'ai réussi à faire une version test avec Premiere Pro, mais dans l'idée il faudrait que je puisse le faire automatiquement avec ffmpeg sur le RPi. Peut-être que le traitement (téléchargement/coupe/fusion des contenu) peut se faire pendant que Philippe est "off" (qu'il ne parcourt pas de contenu) ?
 +
 +
Le résultat n'est pas très satisfaisant. Trop rapide ou trop direct.
 +
 +
===Mise en espace/installation===
 +
 +
Pour l'instant quand je lance Selenium il y a une interface graphique qui se lance, donc je peux voir sans toucher à rien le bot qui s'active et "fait sa vie".
 +
Cette étape est assez fascinante et flippante, c'est un peu comme si Philippe prenait le contrôle et lançait des vidéos comme ça sur mon ordi pendant que je travaille dessus. Alors sur Youtube une fois le premier clic sur une vidéo passé c'est pas très intéressant, mais sur un autre réseau avec plus d'interactivité (scroll par exemple ?) cette vidéo peut-être parlante, et agir presque comme un test de Turing si on la voit sans le contexte.
 +
 +
Problème : comment faire pour que ça n'ait pas l'air d'une simple vidéo en accéléré de quelqu'un sur un ordi ? Une possibilité serait de le montrer en live, et donc de donner un côté performatif à l'expérience.
 +
* Peut-être que le programme peut être lancé sur un ordi (avec crontab ?) qui l'active/l'arrête à des heures fixes (et donc on verrait les prints dans un terminal de cron et du programme, quand selenium n'est pas actif ?
 +
* Il faudrait pouvoir visualiser d'une façon ou d'une autre le clic ou le mouvement de souris, pour retrouver ce côté automatique qu'il y a dans le remplissage du formulaire.
 +
* Peut-être qu'il peut-être streamé en direct depuis cet ordi ? Philippe aurait une chaîne Twitch pour partager sa passion de la recommandation ?
 +
Voir le déroulement en live accentue le côté voyeur, comme si on espionnait quelqu'un devant son ordi pendant qu'il se détend, drôle de sensation. L'objectif est de nous mettre dans une position de voyeur alors qu'on a affaire a un robot.
 +
 +
Matériellement, quel dispositif pour montrer cette vidéo ?
 +
* Il faudrait un environnement de bureau, un fauteuil avec du café/thé ?), personnalisé (peut-être par rapport aux passions que Philippe a acquis pendant l'expérience). Ambiance ''casual'' et intime (le voyeurisme tout ça).
 +
* Quel type d'ordi ? Philippe est un "monsieur tout le monde" [https://www.businessinsider.com/apple-iphones-movies-bad-guys-knives-out-director-rian-johnson-2020-2?IR=C Un article qui peut donner une piste ?]
 +
* Dans une petite pièce, un vrai bureau ou dans une pièce qui n'a rien à voir ?
 +
 +
 +
Peut-être que toutes mes expérimentations d'AP peuvent se retrouver sur ce bureau (éditions, sites/interfaces dans d'autres fenêtres…), et alors on fouillerait dans ses tiroirs et sur son bureau pour enquêter sur lui ? (lien avec le portrait-robot ?)
 +
 +
La question mon alter-ego revient, puisque les intérêts de Philippe seraient alors confondus avec les miens (mon sujet de la recherche de cette année). Il pourrait alors y avoir deux bureaux, un bureau qui est celui de Philippe étant le résultat et fonctionnant indépendamment, et mon bureau, avec les dessous, les projets parallèles et connexes qui viennent nourrir le projet et apporter de l'interactivité (expériencer des sites sans recommandation, aller sur le "blog" de Philippe ou voir les logs du RPi).
 +
 +
===Interface===
 +
L'idée serait peut-être aussi de faire une interface, comme un journal de bord de ce parcours à travers les algorithmes (en modifiant l'interface de la Pibliothèque ?). Avec les logs récupérés (URLs…), je peux mettre en page dynamiquement comme un blog, un agrégat qui serait le reflet de la personnalité de Philippe.
 +
Comment montrer cette personnalité, ce "portrait-robot" ? Comment rendre des logs narratifs ?
 +
 +
Je pourrais éventuellement intégrer une partie d'étude statistique (à partir des tags par exemple) pour essayer de trouver des tendances dans le comportement de Philippe, un peu comme une étude sociologique de son profil. Ça peut renvoyer à la surveillance et au côté analytique des algorithmes, qui construisent ce genre de profil statistique pour construire des recommandations personnalisées, et en même temps être un moyen différent de montrer une personnalité.
 +
 +
Peut-être qu'une façon de rendre compte du parcours est d'enregistrer sous forme de phrases les différentes actions effectuées par le bot (Je clique à tel endroit, j'attends que la vidéo se termine, la page se recharge). Lecture par un text-to-speech pour évoquer l'aide pour les non-voyants (le narrateur vocal ?) ? Voix qui décrit à l'oral le comportement de l'utilisateur ?
 +
 +
J'ai essayé de donner une forme narrative à ce "log" (fichier texte qui regroupe différents messages concernant le fonctionnement d'un programme):
 +
<syntaxhighlight lang="text>
 +
Je me connecte tout seul, c'est bien pratique
 +
Je regarde la vidéo de la dernière fois.
 +
Je clique sur le bouton "play" pour lancer la vidéo.
 +
  Le titre de la vidéo est Et voilà les Shadoks, la saison 2 | Archive INA.
 +
    L'url est [https://www.youtube.com/watch?v=Dk1JjjbZ4yc]
 +
      La vidéo dure 2:18:45.
 +
 +
Une nouvelle vidéo !
 +
  Le titre de la vidéo est Et voilà les Shadoks, la saison 1 | Archive INA.
 +
    L'url est [https://www.youtube.com/watch?v=tpD0Pdr7oD0]
 +
      La vidéo dure 1:33:51.
 +
 +
Une nouvelle vidéo !
 +
  Le titre de la vidéo est Culte : c’était leur 1ère télé, allez-vous les reconnaître ? | Archive INA.
 +
    L'url est [https://www.youtube.com/watch?v=XA3ScOmGr34]
 +
      La vidéo dure 25:29.
 +
 +
Une nouvelle vidéo !
 +
  Le titre de la vidéo est 5 inventions cultes des années 90 | Archive INA.
 +
    L'url est [https://www.youtube.com/watch?v=ueaZgk_qduw]
 +
      La vidéo dure 13:04.
 +
</syntaxhighlight>
 +
 +
Cette interface sera sûrement en trois parties : l'une aura la forme d'un blog, d'un moyen de visualiser temporellement la progression du bot, qui raconte ce qu'il est en train de faire et commente au fur et à mesure les particularités de son parcours. Le blog symbolise aussi une personne qui raconte son expérience personnelle, c'est donc adapté par rapport à ma thématique.</br>
 +
La deuxième partie sera plus visuelle, et sera une séquence des thumbnails des vidéos. C'est une autre manière de ressentir le parcours du bot, de lui donner un aspect narratif par l'image aussi.</br>
 +
La troisième pourra faire le lien entre les deux premières, et sera une couche plus analytique, qui fera ressortir peut-être des éléments statistiques/sociologiques sur la personnalité du bot (en fonction des tags des vidéos par exemple, en utilisant des outils tels qu'Iramuteq ?) ou les catégories YouTube les plus présentes (Divertissement, Éducation, Sport etc.), en tout cas apporter un recul sur le parcours dans sa globalité.
 +
 +
====V1====
 +
Pour l'instant l'interface ressemble à ça :
 +
 +
PARTIE TEXTE</br>
 +
[[Fichier:interface_v1_texte.png|600px]]
 +
 +
PARTIE IMAGES</br>
 +
[[Fichier:interface_v1_images.png|600px]]
 +
 +
Pour passer de l'une à l'autre pour l'instant il faut cliquer sur le bouton "IMAGES".
 +
[[Fichier:interface_v1_toggle.gif]]
 +
 +
La police utilisée est Happy Times at the IKOB New Game Plus Edition de Lucas Bihan. Je cherchais une police serif pour donner un côté plus  "littéraire"/narratif, puisque le bot raconte son parcours. Cette police est une version contemporaine et libre de la Times New Roman.
 +
 +
J'ai rajouté la possibilité d'ouvrir la vidéo en question quand on clique sur le titre directement dans le post.
 +
[[Fichier:interface_v1_clic.png|900px]]
 +
 +
====V2====
 +
Je voulais que sur l'interface on puisse à la fois réagir en live à l'avancement du bot, mais en même temps servir d'archive pour voir la progression dans la durée. Finalement, pour que tout ça soit plus cohérent, les deux parties ne doivent pas forcément cohabiter autant, au moins pas dans la forme qu'à la première version.
 +
 +
Une donnée importante du projet est le temps, puisque le bot fonctionne en temps réel, et regarde entièrement chaque vidéo (et chaque pub).
 +
Il faudrait donc que je trouve comment faire une partie "chargement", ou bien de trouver un moyen de marquer l'évènement "nouvelle vidéo" de façon à créer une attente si l'on reste sur la page. Le but étant de donner en vie d'attendre pour voir quelle sera la prochaine vidéo vue par le bot.
 +
 +
J'aimerais que l'interface ait un côté plus "littéraire/narratif". Pour cela je veux que la partie archive ait plus l'air d'un [https://fr.wikipedia.org/wiki/Journal_intime journal intime] qu'un log ou qu'une page Twitter. C'est plus ce côté "carnet" qui m'intéresse qu'un côté très numérique. J'ai trouvé les sites de [https://www.monkkee.com/fr/ monkkee] et [https://penzu.com/ penzu], qui sont des journaux intimes en ligne, pour essayer de voir la forme que ça prend. En fait ça ressemble juste à un éditeur de texte en ligne, mais on peut personnaliser l'aspect de la page.
 +
 +
[[Fichier:penzu_exemple.jpg]]
 +
[[Fichier:monkey_exemple.jpg|500px]]
 +
 +
Pour passer de la partie live à la partie archive, j'ai pensé à un dispositif comme celui du site de [http://spectorbooks.com/ Spector Books], avec un moyen de glisser avec la souris pour passer de l'un à l'autre comme si on faisait des aller-retours avec les pages d'un livre pour consulter des notes ou un index.
 +
 +
Pour la partie image j'ai pensé qu'au lieu de faire une séquence d'images à la verticale, où on devrait descendre en scrollant, je pourrais faire comme un flip-book. L'image suivante remplacerait la précédente lorsque l'on scrolle.
 +
 +
Maintenant j'utilise la police EB Garamond, une version numérisée et libre d'une Garamond, qui conserve des aspérités ou imperfections de l'impression au plomb.
 +
 +
En observant les gabarits de Facebook, Twitter ou YouTube j'ai remarqué qu'il y a souvent 2 ou 3 colonnes qui fonctionnent indépendamment les unes des autres (on peut scroller dans chacune d'elle séparément). La colonne du milieu est toujours là où se trouve le principal, les autres colonnes servant à afficher des éléments supplémentaires, des liens pour la navigation, des pubs ou des contenus recommandés. Je voudrais donc partir de cette configuration un peu comme base.
  
==Début==
+
==Partie technique==
 +
===Collecte===
 +
====Extension Javascript====
 
J'ai d'abord essayé de faire une extension de navigateur pour Firefox, qui enregistre l'URL à chaque fois que la lecture automatique change de vidéo (j'ai commencé par YouTube) et qui m'envoie la liste par mail tous les 10 vidéos (les extensions n'ont pas d'accès à la possibilité d'écrire ou de lire sur un fichier local). Pour ça j'ai utilisé les Mutation Observer de javascript, qui permettent de lancer des instructions quand certaines mutations apparaissent dans le DOM, ainsi que la librairie smtp.js, qui grâce au serveur smtp de Gmail, me permet de m'envoyer des mails.
 
J'ai d'abord essayé de faire une extension de navigateur pour Firefox, qui enregistre l'URL à chaque fois que la lecture automatique change de vidéo (j'ai commencé par YouTube) et qui m'envoie la liste par mail tous les 10 vidéos (les extensions n'ont pas d'accès à la possibilité d'écrire ou de lire sur un fichier local). Pour ça j'ai utilisé les Mutation Observer de javascript, qui permettent de lancer des instructions quand certaines mutations apparaissent dans le DOM, ainsi que la librairie smtp.js, qui grâce au serveur smtp de Gmail, me permet de m'envoyer des mails.
 +
Les extensions de navigateurs fonctionnent de la même façon sur Chrome ou Firefox :
 +
* le fichier manifest.json sert à régler différents paramètres. Il peut par exemple servir de filtre pour choisir les onglets ou les noms de domaines dans lesquels les scripts s'éxécutent
 +
* le "content-script" (ici espion.js) peut accéder au contenu du DOM de la page dans laquelle il est lancé (récupérer des infos et modifier la page en js). Il peut envoyer des informations au "background script" et vice-versa
 +
* le script background.js peut effectuer plus d'actions (avoir accès à des APIs ou ce genre de choses) mais ne peut pas accéder au DOM
 +
 +
=====manifest.json=====
 +
<syntaxhighlight lang="json">
 +
{
 +
"manifest_version": 2,
 +
  "name": "Espion",
 +
  "version": "1.0",
 +
 +
  "description": "Enregistre toutes les urls des vidéos lancées par l'onglet up next de Youtube et les musiques de Spotify",
 +
 +
  "background": {
 +
    "scripts": ["background.js"]
 +
  },
 +
 +
  "icons": {
 +
    "48": "icons/border-48.png"
 +
  },
 +
 +
  "content_scripts": [
 +
    {
 +
      "matches": ["*://*.youtube.com/*", "*://*.spotify.com/*"],
 +
      "js": ["espion.js"]
 +
    }
 +
  ]
 +
}
 +
 +
</syntaxhighlight>
 +
 +
=====espion.js=====
 +
<syntaxhighlight lang="javascript">
 +
///////////////////////////FONCTIONS/////////////////////////////////
 +
function handleResponse(message) {
 +
  console.log(`${message.response}`);
 +
}
 +
 +
function handleError(error) {
 +
  console.log(`Error: ${error}`);
 +
}
 +
 +
function notifyBackgroundPage(e) {
 +
  var sending = browser.runtime.sendMessage({
 +
    test: e
 +
  });
 +
  sending.then(handleResponse, handleError);
 +
}
 +
 +
/////////////////////SCRIPT///////////////////////////////////////////
 +
console.log('coucou');
 +
 +
var liens = [];
 +
var txt;
 +
var count = 0;
 +
 +
var observer = new MutationObserver(function(mutations) {
 +
 +
  // For the sake of...observation...let's output the mutation to console to see how this all works
 +
liens.push(window.location.href);
 +
  console.log('yes');
 +
 +
  if (count == 4) {
 +
    notifyBackgroundPage(liens);
 +
    liens = [];
 +
    console.log(liens);
 +
  }
 +
 +
  count = (count+1)%5;
 +
  console.log(count);
 +
 +
});
 +
 +
// Notify me of everything!
 +
var observerConfig = {
 +
childList: true,
 +
characterData: true,
 +
};
 +
 +
// Node, config
 +
var targetNode = document.getElementById('movie_player');
 +
 +
observer.observe(targetNode, observerConfig);
 +
 +
</syntaxhighlight>
 +
 +
=====background.js=====
 +
<syntaxhighlight lang="javascript">
 +
/* SmtpJS.com - v3.0.0 */
 +
var Email = { send: function (a) { return new Promise(function (n, e) { a.nocache = Math.floor(1e6 * Math.random() + 1), a.Action = "Send"; var t = JSON.stringify(a); Email.ajaxPost("https://smtpjs.com/v3/smtpjs.aspx?", t, function (e) { n(e) }) }) }, ajaxPost: function (e, n, t) { var a = Email.createCORSRequest("POST", e); a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), a.onload = function () { var e = a.responseText; null != t && t(e) }, a.send(n) }, ajax: function (e, n) { var t = Email.createCORSRequest("GET", e); t.onload = function () { var e = t.responseText; null != n && n(e) }, t.send() }, createCORSRequest: function (e, n) { var t = new XMLHttpRequest; return "withCredentials" in t ? t.open(e, n, !0) : "undefined" != typeof XDomainRequest ? (t = new XDomainRequest).open(e, n) : t = null, t } };
 +
 +
var i = 0;
 +
 +
function sendEmail(txt) {
 +
i += 1;
 +
Email.send({
 +
Host: "smtp.gmail.com",
 +
Username : "***************************",
 +
Password : "**********",
 +
To : "adresse@mail.destinataire",
 +
From : "adresse_expéditeur@gmail.com",
 +
Subject : "youtube" + i,
 +
Body : txt,
 +
}).then(
 +
message = console.log("Mail bien envoyé !")
 +
);
 +
}
 +
 +
console.log('salut');
 +
 +
function handleMessage(request, sender, sendResponse) {
 +
  console.log(request.test);
 +
  sendResponse({response: "Bien reçu !"});
 +
sendEmail(request.test);
 +
}
 +
 +
browser.runtime.onMessage.addListener(handleMessage);
 +
 +
</syntaxhighlight>
 +
 
Le problème est que la version de Firefox sur Raspbian n'est pas optimisée pour le Raspberry Pi, et Firefox plante assez rapidement. En essayant avec Chromium (qui est apparemment optimisé au maximum), ça marche beaucoup mieux mais ça plante aussi quand le mail s'envoie.
 
Le problème est que la version de Firefox sur Raspbian n'est pas optimisée pour le Raspberry Pi, et Firefox plante assez rapidement. En essayant avec Chromium (qui est apparemment optimisé au maximum), ça marche beaucoup mieux mais ça plante aussi quand le mail s'envoie.
  
 +
J'ai quand même réussi a choper le lien de 22 vidéos à la suite avant que ça plante, en bidouillant un peu.
 +
Avec le paquet youtube-dl [https://github.com/ytdl-org/youtube-dl/blob/master/README.md#readme] (sur Linux, je sais pas s'il existe ailleurs) on peut en une commande télécharger toutes les vidéos si elles sont dans un fichier texte correctement formaté.
 +
 +
====Selenium (Python)====
 
Je vais essayer avec Selenium, qui est un navigateur utilisable par ligne de commande, qui peut aussi s'utiliser 'headless', c'est-à-dire sans interface graphique. On peut l'utiliser avec Python et donc le lancer automatiquement dès que je branche le Raspberry Pi.
 
Je vais essayer avec Selenium, qui est un navigateur utilisable par ligne de commande, qui peut aussi s'utiliser 'headless', c'est-à-dire sans interface graphique. On peut l'utiliser avec Python et donc le lancer automatiquement dès que je branche le Raspberry Pi.
  
Ligne 554 : Ligne 1 231 :
 
from datetime import datetime
 
from datetime import datetime
  
USERNAME = 'philippe22philippe@gmail.com'
+
USERNAME = 'adresse.mail@gmail.com'
PASSWORD = 'ph111li@Ps'
+
PASSWORD = 'mot_de_passe'
  
 
options = webdriver.ChromeOptions()
 
options = webdriver.ChromeOptions()
Ligne 669 : Ligne 1 346 :
 
     WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
 
     WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
 
     print('nouvelle vidéo !')
 
     print('nouvelle vidéo !')
     vid()
+
 
 +
try:
 +
     while True:
 +
        vid()
 +
except KeyboardInterrupt:
 +
    raise
 +
except:
 +
    print('Le programme a planté :/')
 +
 
 +
</syntaxhighlight>
 +
 
 +
Peut être que pour des plateformes comme Facebook ou Twitter je peux utiliser un système de bot (pouvant être hébergé sur le RPi quand il est en fonctionnement) plutôt qu'un protocole complexe sur Selenium ?
 +
Naviguer entre les utilisateurs en les taggant ou en retweetant leurs posts ? -> La page Twitter en soi est un témoin de son passage et de la construction d'une personnalité, d'un avis.
 +
 
 +
Pour l'instant les pubs YouTube font tout planter, j'essaie de voir comment importer une extension dans Selenium (au moins temporairement) pour pouvoir mettre au point le truc sans les pubs.
 +
J'ai réussi à empaqueter des extensions au format .crx pour pouvoir lancer Selenium avec cette extension (AdBlocker), mais ça ne fonctionne pas et Selenium plante tout simplement.
 +
 
 +
J'essaie de régler le problème de mon programme qui n'arrive pas à cliquer sur "Skip Ad" (peut-être un problème de sélecteur CSS/Xpath).
 +
Ça à l'air de marcher avec ce sélecteur (pour le bouton "Skip Ad") :
 +
<syntaxhighlight lang="python">
 +
"#skip-button\:8 > span > button"
 +
</syntaxhighlight>
 +
(Même si je n'ai pas trouvé à quoi correspond le :8)
 +
 
 +
Un autre problème apparaît maintenant (23/04/2020) : Google a mis à jour la détection de robots/automates, et donc le moyen que j'avais trouvé pour contourner la sécurité de Youtube ne marche plus. Quand je veux me connecter, Youtube repère que j'utilise Selenium et considère que ce n'est pas sécurisé.
 +
 
 +
[[fichier:ça_marche_plus.png|800px]]
 +
 
 +
Sur l'image on peut voir à droite de la barre d'URL que les cookies ne sont pas activés, peut-être que le problème est que Selenium par défaut crée des sessions qui ne ne laissent pas de cookies ?
 +
 
 +
J'ai peut-être une piste qui utiliserait les cookies de connexion, pour me connecter automatiquement sans passer par la page de Google qui détecte Selenium.
 +
 
 +
=====Autre solution ?=====
 +
Apparemment c'est possible de créer un profil utilisateur sur Selenium qui est gardé en mémoire dans un dossier, et qui comprend les cookies (et donc les infos de connexion) ainsi que les extensions. Si j'arrive à faire fonctionner ça j'élimine le problème de la connexion et des pubs en même temps.[https://stackoverflow.com/questions/15058462/how-to-save-and-load-cookies-using-python-selenium-webdriver]
 +
Mais le souci reste le même : il faut que je puisse me connecter une première fois dans la page ouverte par Selenium pour enregistrer toutes les infos. Le navigateur ne détecte plus que je suis un robot, mais il ne me laisse pas me connecter.
 +
 
 +
=====Cookies ?=====
 +
Selenium permet de recupérer les cookies d'une page avec browser.get_cookies(), mais le problème c'est qu'il faut déjà être connecté pour récupérer les cookies de connexion.[https://stackoverflow.com/questions/45417335/python-use-cookie-to-login-with-selenium]Donc il faut que je me connecte avec Selenium pour récupérer les cookies qui me permettront de me connecter sans la page de connexion avec Selenium. Puisque je ne peux pas me connecter même manuellement dans la page ouverte par Selenium, je ne peux pas récupérer les cookies de cette façon.
 +
 
 +
Pour contourner ça j'ai essayé de récupérer les cookies depuis une session manuelle de Chromium. [https://github.com/dandv/convert-chrome-cookies-to-netscape-format]
 +
J'arrive à avoir un fichier de cookies au format netscape, mais Selenium n'accepte que les fichiers de cookies sous forme d'objets JSON "sérialisés", donc je cherche maintenant un moyen de conversion.
 +
J'ai trouvé [https://coockie.pro/pages/netscapetojson/ ce site] (en russe) qui permet cette conversion.
 +
Un objet sérialisé est une variable convertie sous forme de fichier ou de chaîne de caractères qui peut donc être partagée et réutilisée dans un autre script. La sérialisation c'est le processus de conversion d'un objet dans un format binaire transportable et reconstructible pour créer un clone de cet objet dans un autre programme. La désérialisation c'est l'étape de reconstruction de l'objet dans le programme.
 +
En Python pour faire ça on utilise le module [https://docs.python.org/3/library/pickle.html Pickle]. Apparemment il n'est pas sécurisé, mais vu que je ne vais l'utiliser en local c'est pas très grave.
 +
 
 +
J'ai réussi la conversion, je copie-colle dans pickling.py les cookies au format JSON, je change quelques trucs (transformer les objets JSON 'true', 'false' et 'null' respectivement en 'True', 'False' et 'None', qui sont les équivalents en Python) et je "pickle" le résultat dans cookies.pkl :
 +
<syntaxhighlight lang="python">
 +
pickle.dump(to_pickle, open("cookies.pkl","wb"))
 +
</syntaxhighlight>
 +
Le mode "wb" est pour "write binary", puisque pickle convertit l'objet en binaire pour l'enregistrer.
 +
 
 +
On obtient bien un truc illisible:
 +
<syntaxhighlight lang="text">
 +
��]q
 +
</syntaxhighlight>
 +
(je ne peux pas tout copier apparemment).
 +
 
 +
Et pour "unpickle" dans mon programme principal :
 +
<syntaxhighlight lang="python">
 +
cookies = pickle.load(open("cookies.pkl", "rb"))
 +
</syntaxhighlight>
 +
 
 +
Quand j'importe les cookies dans Selenium ça plante (apparemment un des cookies a un mauvais nom), j'essaie de voir lequel pour corriger ça.
 +
 
 +
Petit debugging :
 +
<syntaxhighlight lang="python">
 +
for i, cookie in enumerate(cookies):
 +
    try:
 +
        browser.add_cookie(cookie)
 +
    except:
 +
        print('Le cookie %s ne marche pas' % i)
 +
</syntaxhighlight>
 +
 
 +
C'est le dernier cookie qui ne marche pas, j'ai sûrement copié-collé une ligne vide en trop.
 +
J'ai aussi enlevé la première qui était juste la ligne disant que le cookie était au format Netscape.
 +
 
 +
Les cookies sont maintenant importés. Il faut sûrement recharger la page pour qu'ils prennent effet:
 +
<syntaxhighlight lang="text">
 +
browser.refresh()
 +
</syntaxhighlight>
 +
Et ça marche :).
 +
 
 +
J'arrive à me connecter sur YouTube à nouveau, même si c'est moins impressionnant que de voir les formulaires qui se remplissent tous seuls.
 +
 
 +
Je vais alors essayer de créer un profil utilisateur pour inclure les extensions qui bloquent les pubs.
 +
 
 +
===Passer les pubs===
 +
Je crois que j'avais simplement oublié de désactiver l'option --disable extensions, ce qui faisait planter Selenium quand je chargeais une extension. J'arrive maintenant à charger des extensions sans soucis (oups).
 +
 
 +
===Petites corrections===
 +
Pour corriger le problème du dédoublage des liens dans le log (quand une nouvelle vidéo se lance, le programme détecte 6-7 changements d'URL et relance la boucle 6-7 fois, ce qui est absurde. Il suffit de rajouter sleep(1) à la fin de la boucle pour prendre en compte le chargement de la nouvelle page.
 +
<syntaxhighlight lang="python">
 +
def new_vid():
 +
    """Fonction qui lance un protocole à chaque changement de page."""
 +
    get_infos()
 +
    time, tps = duree_vid()
 +
    print('      La vidéo dure %s.' % tps)  #NARRATIF
 +
    upnext = browser.find_element_by_css_selector('#dismissable > div > div.metadata.style-scope.ytd-compact-video-renderer > a')
 +
    upnext = upnext.get_attribute("href")
 +
    print('Je regarde la vidéo jusqu\'au bout.')
 +
    WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
 +
    print('\nUne nouvelle vidéo !')  #NARRATIF
 +
    sleep(1)
 +
</syntaxhighlight>
 +
 
 +
Ensuite il faut corriger le problème de la duplication du dernier lien (quand on ouvre la dernière vidéo on ré-enregistre son URL).
 +
Au moment de l'enregistrement on vérifie que l'URL actuelle n'est pas la même que la dernière de la liste.
 +
 
 +
<syntaxhighlight lang="python">
 +
with open('log_urls.txt', 'a+', encoding='utf8') as f_2:
 +
    last_vids = f_2.read()
 +
    last_vids = last_vids.splitlines()
 +
    if last_vids[len(last_vids)-1] != browser.current_url:
 +
        f_2.write(browser.current_url + '\n')
 +
</syntaxhighlight>
 +
 
 +
===Headless ?===
 +
Maintenant je veux essayer de faire tourner le programme en mode "headless" (sans interface graphique), pour que ce soit plus rapide et moins gourmand, pour pas que ça ne fasse planter le Raspberry.
 +
Il faut juste rajouter
 +
<syntaxhighlight lang="python">
 +
options.add_argument("headless")
 +
</syntaxhighlight>
 +
dans les options de Selenium.
 +
 
 +
Chrome et Firefox ont tous les deux des modes headless, mais pour Chrome dans ce cas-là on ne peut pas utiliser d'extensions, et il faudrait que je passe les pubs manuellement en cliquant sur "Skip Ad".</br>
 +
On peut apparemment utiliser des extensions dans Firefox, mais Firefox est mal optimisé pour le Raspberry. Je pourrais essayer mais il faut que je réfléchisse à la connotation donnée par ces choix en fonction de la personnalité de "Philippe".
 +
*Avec Firefox ce serait plus facile à mettre en place, mais le choix de Firefox en tant qu'utilisateur sous-entend peut-être une personnalité qui se soucie de sa vie privée sur Internet.
 +
*Chromium demanderait que je fasse de la bidouille pour passer les pubs manuellement (ce qui serait plus en accord avec l'idée d'un. utilisateur.ice lambda, qui n'a pas forcément d'adblocker) mais serait plus logique (Chrome est le navigateur le plus utilisé, + de 64% des gens l'utilisent en Novembre 2019 d'après Wikipédia).
 +
 
 +
Ce n'est pas vraiment nécessaire que mon bot tourne en headless, puisque pour l'instant il va tourner chez moi.
 +
 
 +
===Choix de la vidéo===
 +
Je vais le baser le choix de la première vidéo lancée par le bot sur le nombre de caractères en capitales dans le titre.
 +
<syntaxhighlight lang="python">
 +
###fonction pour calculer la proportion de capitale dans une string
 +
def nbr_uppr(string):
 +
    uppercase = 0
 +
    for c in string:
 +
        if c.isupper():
 +
            uppercase += 1
 +
        else:
 +
            pass
 +
    proportion = uppercase/len(string)
 +
    return proportion
 +
 
 +
###récupère tous les titres de vidéos de la page d'accueil
 +
browser.get('https://www.youtube.com/')
 +
titres = browser.find_elements_by_id('video-title')
 +
        proportion_upper = []
 +
        ###sélectionne seulement les 8 premiers titres (ceux que l'on peut voir quand on ouvre la page d'accueil)
 +
        for r in range(0,8):
 +
            str_titre = str(titres[r].get_attribute('textContent'))
 +
            proportion_upper.append(nbr_uppr(str_titre))
 +
        ###max(array) retourne la valeur la plus haute de l'array
 +
        max_index = proportion_upper.index(max(proportion_upper))
 +
        titres[max_index].click()
 +
</syntaxhighlight>
 +
 
 +
===Sur le Raspberry===
 +
Maintenant que le programme fonctionne sur mon ordi, il faut le transférer sur mon ordi. J'ai essayé de refaire la même démarche que j'avais effectuée sur mon ordi, mais les chromedrivers disponibles au téléchargement sont adaptés à une architecture x64, alors que le RPi a une architecture x32. Je suis tombé sur [https://www.reddit.com/r/selenium/comments/7341wt/success_how_to_run_selenium_chrome_webdriver_on/ ce thread reddit] qui explique pas à pas comment faire pour avoir un chromedriver adapté au RPi (x32 et armhf). Le driver que j'ai trouvé est la version 65 et quelques, alors que Chromium est en version 78 sur le Rpi. Ça n'a pas l'air de poser de soucis pour l'instant.
 +
 
 +
Il faut maintenant que j'adapte un peu le code pour qu'il tourne sur le RPi (beaucoup plus lent, il faut rajouter des conditions pour être sûr que les éléments soient chargés avant d'essayer d'y accéder).
 +
 
 +
Le programme tourne sur le RPi, vraiment très lentement mais ça fonctionne. Malheureusement il plante au bout d'un moment. D'après mes tests parfois c'est Chromium qui crashe, parfois c'est c'est juste que la page ne charge pas bien. C'est à moitié un problème de connexion et à moitié un problème de mémoire peut-être. Je vais peut-être essayer de le faire tourner en headless.
 +
 
 +
Pour faire tourner le programme en headless il faut enlever l'extension qui permet de bloquer les pubs. Avant ça me posait problème parce qu'il fallait absolument que je passe les pubs pour récupérer la durée de la vidéo (et pas la durée de la pub), comme ça je pouvait indiquer à Selenium le temps d'attente avant qu'une nouvelle vidéo apparaisse. Mais en fait je peux mettre un temps très long (par exemple 5h) pour être sûr que le programme attende bien sagement la fin de la vidéo pour détecter le changement de vidéo, qui s'opère tout seul.
 +
 
 +
Sur mon ordi mon programme marche en headless, je n'ai plus qu'à le mettre sur mon RPi et le brancher en ethernet et c'est parti !
 +
Pour le faire fonctionner sur le RPi, il faut modifier un peu le script. À un moment j'utilise un bloc except sans préciser d'erreur, ce qui fait que le programme ne plante pas s'il y a un problème à ce moment-là. Et puisqu'il tourne en headless, je ne peux pas voir s'il a planté.
 +
 
 +
Je modifie donc
 +
 
 +
<syntaxhighlight lang=python">
 +
###cliquer sur le bouton play pour lancer la première vidéo
 +
try:
 +
  elem = browser.fin_elements_by_css_selector("#movie_player > div.ytp-cued-thumbnail-overlay > button")
 +
  elem.click()
 +
except:
 +
  continue
 +
</syntaxhighlight>
 +
 
 +
par
 +
 
 +
<syntaxhighlight lang=python">
 +
###cliquer sur le bouton play pour lancer la première vidéo
 +
elem = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#movie_player > div.ytp-cued-thumbnail-overlay > button")))
 +
elem.click()
 +
</syntaxhighlight>
 +
qui attend que le bouton "play" soit chargé pour cliquer dessus et lancer la vidéo.
 +
https://serverfault.com/questions/96499/how-to-automatically-start-supervisord-on-linux-ubuntu
 +
Il me reste à trouver un moyen de "surveiller" l'activité du programme, pour le relancer automatiquement s'il plante.</br>
 +
Pour cela je vais utiliser [http://supervisord.org/ supervisor], qui me permet de transformer un programme en "daemon", c'est-à-dire en tâche fonctionnant en arrière-plan. Je peux le paramétrer pour définir le nombre de fois que supervisor doit essayer de relancer le programme s'il plante, le chemin pour des fichiers de log etc.
 +
 
 +
Puisque le programme sera lancé par un autre programme (et donc pas depuis le dossier de travail), il faut que dans le script Python tous les chemins soient noté en absolu, et pas relativement au dossier dans lequel se trouve le programme (sinon ça va planter lorsque le script cherchera les cookies ou les logs par exemple).
 +
 
 +
Dans le fichier de configuration de supervisor (/etc/supervisord.conf) je rajoute
 +
<syntaxhighlight lang="text">
 +
[program:portrait-robot]
 +
command=python3 ~/Documents/BOULOT/AP/portrait-robot/portrait_robot.py
 +
priority=0
 +
startsecs=50
 +
startretries=5
 +
stdout_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_log.txt
 +
stderr_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_err.txt
 +
</syntaxhighlight>
 +
qui correspondent aux paramètres énoncés plus haut.
 +
 
 +
Maintenant, il faut que je fasse en sorte que supervisor (et donc mon programme en python) se lance tout seul à chaque démarrage du RPi.
 +
Je suis les instructions de [https://serverfault.com/questions/96499/how-to-automatically-start-supervisord-on-linux-ubuntu cette page].
 +
 
 +
--------------
 +
 
 +
Après un essai concluant je me rends compte que les vidéos trouvées par le Raspberry sont toutes en anglais, qui doit être par défaut le paramètre du compte Google. Il faut que je change ça et que je recommence pour avoir des vidéos en français.</br>
 +
J'ai changé les paramètres du compte Google et refait les manipulations pour obtenir les cookies, et cette fois-ci effectivement j'ai des résultats en français sur la page d'accueil.
 +
 
 +
===Interface===
 +
====Structure====
 +
=====Essai avec javascript=====
 +
Je suis en train de construire une interface en PHP/javascript pour extraire les urls du log généré par le programme, et en extraire des informations (titre, durée, chaîne, thumbnails etc.) grâce à l'API de YouTube pour pouvoir les mettre en forme dynamiquement. ces informations me serviront à faire une "analyse" du contenu au moment où je le collecte. Je pense qu'une partie prendra une forme textuelle, comme une narration par le bot des actions effectuées (le log du programme python fait ça en partie, mais de manière très scriptée).</br>
 +
Le but est que le bot commente par des phrases du style "Hmmm j'en ai marre celle-là ça fait X fois que je tombe dessus", ou alors "J'aime bien les longues vidéos" si il y a 3 fois d'affilée des vidéos de plus de 2h, ce genre de choses.
 +
 
 +
Je récupère le contenu du fichier texte et l'affiche ligne par ligne dans des balises p :
 +
<syntaxhighlight lang="php">
 +
  $fh = fopen('log_urls.txt','r');
 +
    while ($line = fgets($fh)) {
 +
      echo('<p class="url">'.$line.'</br></p>');
 +
    }
 +
    fclose($fh);
 +
</syntaxhighlight>
 +
 
 +
Je rends ces balises invisibles :
 +
<syntaxhighlight lang="css">
 +
.url{
 +
  display:none;
 +
}
 +
</syntaxhighlight>
 +
 
 +
Je récupère le contenu (présent bien qu'invisible) dans une array :
 +
<syntaxhighlight lang="javascript">
 +
var urls_collection = document.getElementsByClassName('url');
 +
urls_array = Array.from(urls_collection);
 +
urls = [];
 +
//faire des éléments HTML récupérés une array d'urls
 +
for (url of urls_array){
 +
  //il faut enlever le caractère "\n" à la fin de chaque ligne
 +
  strippedUrl = url.textContent.trim()
 +
  urls.push(strippedUrl);
 +
}
 +
</syntaxhighlight>
 +
 
 +
Je fais appel à l'API de YouTube avec une librairie javascript :
 +
<syntaxhighlight lang="javascript">
 +
function loadClient() {
 +
  gapi.client.setApiKey(apiKey);
 +
  return gapi.client.load("https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest")
 +
    .then(function() {
 +
        console.log("GAPI client loaded for API");
 +
      },
 +
      function(err) {
 +
        console.error("Error loading GAPI client for API", err);
 +
      });
 +
}
 +
// Make sure the client is loaded and sign-in is complete before calling this method
 +
function execute(url) {
 +
  vid_id = url.replace('https://www.youtube.com/watch?v=', '');
 +
  return gapi.client.youtube.videos.list({
 +
      "part": "snippet",
 +
      "id": vid_id
 +
    })
 +
    .then(function(response) {
 +
        // Handle the results here (response.result has the parsed body).
 +
        result = response.result.items['0'].snippet.title;
 +
        // console.log('TITRE : '+result);
 +
        // console.log(typeof ''+result);
 +
        console.log(result)
 +
        return {
 +
          result:result
 +
        };
 +
      },
 +
      function(err) {
 +
        console.error("Execute error", err);
 +
      });
 +
}
 +
</syntaxhighlight>
 +
Il faut aussi charger auparavant la librairie dans la page HTML/PHP.
 +
<syntaxhighlight lang="html">
 +
<script src="https://apis.google.com/js/api.js" type="text/javascript"></script>
 +
</syntaxhighlight>
 +
 
 +
Finalement, cette technique est compliquée puisqu'elle implique des histoires de code asynchrone, et de trouver un moyen pour être sûr que les actions de javascript (appels à l'API) se fassent dans le bon ordre, sinon ça ne fonctionne pas. En plus, cela voudrait dire qu'à chaque chargement de la page il y a autant de requêtes à l'API qu'il y a de vidéos collectées par le bot, ce qui va vite devenir ingérable (et très mal optimisé). Pour finir, si une vidéo est supprimée au moment de la consultation de la page, l'API ne pourra pas la trouver.
 +
 
 +
=====Appeler l'API avec Python=====
 +
Le moyen le plus fiable/efficace est donc de récupérer les infos sur la vidéo (titre, durée, date de publication, description, commentaires, etc.) toujours avec l'API mais via Python/Selenium. De cette façon, on ne fait une requête à l'API qu'une fois par jour, l'interface Python est plus compréhensible pour moi, et je peux enregistrer le résultat au format JSON (qui sera très pratique à traiter en javascript ou en PHP).</br>
 +
J'obtiendrais donc un log en JSON sur le Raspberry, qui se remplira petit à petit. Ensuite il suffit que le Raspberry envoie sur le serveur au fur et à mesure le contenu du log, et ainsi je peux mettre en page dynamiquement ce contenu grâce à javascript et PHP.
 +
 
 +
À la place de simplement enregistrer l'url courante dans un fichier texte, je rajoute cette partie dans mon programme principal en Python (le script qui utilise Selenium) :
 +
<syntaxhighlight lang="python">
 +
import json
 +
import googleapiclient.discovery
 +
import googleapiclient.errors
 +
 
 +
with open('API.txt', 'r', encoding="utf-8") as f:
 +
    api_key = f.read().rstrip()
 +
 
 +
#authentification et configuration de l'API
 +
api_service_name = "youtube"
 +
api_version = "v3"
 +
max_results = 1
 +
 
 +
with open('API.txt', 'r', encoding="utf-8") as f:
 +
    DEVELOPER_KEY = f.read().rstrip()
 +
 
 +
youtube = googleapiclient.discovery.build(
 +
    api_service_name, api_version, developerKey=DEVELOPER_KEY)
 +
 
 +
#la requête à l'API en elle-même
 +
request = youtube.videos().list(
 +
    part="snippet,contentDetails,statistics",
 +
    id="L_LUpnjgPso",
 +
)
 +
response = request.execute()['items'][0]
 +
 
 +
###supprimer les éléments inutiles pour alléger le fichier au maximum
 +
supp = ['kind', 'etag', 'id']
 +
for s in supp:
 +
    del response[s]
 +
 
 +
supp2 = ['channelId', 'liveBroadcastContent', 'localized']
 +
for s2 in supp2:
 +
    del response['snippet'][s2]
 +
 
 +
supp3 = ['dimension', 'projection', 'contentRating', 'licensedContent']
 +
for s3 in supp3:
 +
    del response['contentDetails'][s3]
 +
 
 +
###trouver le thumbnail avec les plus grande définition pour chaque url
 +
thumb_sizes = ['maxres', 'high', 'medium', 'standard', 'default']
 +
for thumb in thumb_sizes:
 +
    if thumb in response['snippet']['thumbnails']:
 +
        ###on utilise la liste temp_list car on ne peut pas supprimer des éléments du dictionnaire directement pendant son itération
 +
        temp_list = []
 +
        for element in response['snippet']['thumbnails']:
 +
            if thumb != element:
 +
                temp_list.append(element)
 +
        for ar in temp_list:
 +
            del response['snippet']['thumbnails'][ar]
 +
        break
 +
 
 +
###changer le categoryId par une chaîne de caractère compréhensible
 +
with open('catégoriesVidéos.json', 'r', encoding='utf-8') as f:
 +
    data = f.read()
 +
    video_categories = json.loads(data)['items']
 +
 
 +
    for cat in video_categories:
 +
        if response['snippet']['categoryId'] == cat['id']:
 +
            response['snippet']['categoryId'] = cat['title']
 +
 
 +
###simplifier un peu le format de la durée de la vidéo
 +
durée_simple = response['contentDetails']['duration'].lstrip('PT').lower()
 +
response['contentDetails']['duration'] = durée_simple
 +
 
 +
###enregistrer le tout dans un fichier JSON
 +
with open('result_API.json', 'a', encoding="utf8") as fp:
 +
    json.dump(response, fp)
 +
    fp.write('\n')
 +
</syntaxhighlight>
 +
J'ai essayé de nettoyer au maximum le fichier JSON pour que ce soit le plus rapide possible lorsque le RPi va envoyer des données sur le serveur. Vu que je suis plus à l'aise en Python ça me permet aussi de ne pas galérer en javascript ou en PHP pour faire la même chose.
 +
 
 +
J'ai utilise [https://developers.google.com/youtube/v3/docs/videoCategories/list?apix=true&apix_params=%7B%22part%22%3A%5B%22snippet%22%5D%2C%22hl%22%3A%22fr%22%2C%22regionCode%22%3A%22BE%22%7D cette fonction de l'API de YouTube] pour récupérer le fichier catégoriesVidéos.json, qui me permet d'avoir la catégorie de la vidéo en français, au lieu d'un code à 2 chiffres.</br>
 +
J'ai quand même nettoyé un peu le fichier JSON pour avoir quelque chose de plus clair, et plus simple pour travailler.
 +
 
 +
Il ressemble à ça :
 +
<syntaxhighlight lang="json">
 +
{
 +
  "items": [{
 +
      "id": "1",
 +
      "title": "Films et animations"
 +
    },
 +
    {
 +
      "id": "2",
 +
      "title": "Auto/Moto"
 +
    },
 +
    {
 +
      "id": "10",
 +
      "title": "Musique"
 +
    },
 +
    {
 +
      "id": "15",
 +
      "title": "Animaux"
 +
    },
 +
    {
 +
      "id": "17",
 +
      "title": "Sport"
 +
    },
 +
 +
</syntaxhighlight>
 +
 
 +
On obtient alors pour chaque vidéo (à partir d'une url) ce genre d'objet JSON :
 +
<syntaxhighlight lang="json">
 +
{
 +
"snippet":
 +
  {"publishedAt":
 +
  "2016-10-02T14:05:46Z",
 +
  "title": "Fireplace 10 hours full HD",
 +
  "description": "Fireplace 10 hours full HD for a romantic moment.\n10 hours burning logs loop play.",
 +
  "thumbnails":
 +
    {"maxres":
 +
      {"url": "https://i.ytimg.com/vi/L_LUpnjgPso/maxresdefault.jpg",
 +
      "width": 1280,
 +
      "height": 720}},
 +
  "channelTitle": "Fireplace 10 hours",
 +
  "tags":
 +
    ["fireplace 10 hours",
 +
    "fireplaces",
 +
    "fireplace video hd",
 +
    "fireplace video",
 +
    "HD fireplace",
 +
    "fireplace burning",
 +
    "burning logs", 
 +
    "fireplace"],
 +
    "categoryId": "Divertissement"},
 +
"contentDetails":
 +
  {"duration": "10h1m26s",
 +
  "definition": "hd",
 +
  "caption": "false"},
 +
"statistics":
 +
  {"viewCount": "21852887",
 +
  "likeCount": "55694",
 +
  "dislikeCount": "4133",
 +
  "favoriteCount": "0"
 +
}}
 +
</syntaxhighlight>
 +
Peut-être que toutes ces informations ne me serviront pas et que j'en enlèverais plus tard, pour réduire encore la taille des fichiers envoyés.
 +
 
 +
=====Affichage dans la page PHP=====
 +
Pour afficher ces informations j'utilise PHP afin de créer une structure HTML :
 +
<syntaxhighlight lang="php">
 +
    <?php
 +
      $arr_JSON = [];
 +
      $fh = fopen('/chemin/log_results.json', 'r');
 +
      while ($line = fgets($fh)) {
 +
        array_push($arr_JSON, json_decode($line, TRUE));
 +
      }
 +
      fclose($fh);
 +
 
 +
      //on inverse l'ordre de l'array pour afficher les dernières vidéos en haut de la page (comme un blog)
 +
      $arr_JSON = array_reverse($arr_JSON);
 +
      $thumb_res = ['maxres', 'high', 'medium', 'default'];
 +
      foreach($arr_JSON as $JSON) {
 +
        echo '<article class="post">';
 +
          echo '<aside class="date">'.$JSON['datetime']['heure'].'</br>'.$JSON['datetime']['jour'].'</aside>';
 +
          echo '<div class="vid">';
 +
            echo '<p>
 +
                  Je regarde une nouvelle vidéo. </br>
 +
                  Le titre est <span class="titre">'.$JSON['snippet']['title'].'.</span></br>
 +
                  Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'.</span>
 +
                  </p>';
 +
                  //trouve l'image dans la plus grande résolution possible
 +
                  foreach ($thumb_res as $res) {
 +
                    if (array_key_exists($res, $JSON['snippet']['thumbnails'])){
 +
                      echo '<img class="hidden" src="'.$JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">';
 +
                      break;
 +
                    } else {
 +
                      continue;
 +
                    }
 +
                  }
 +
          echo '</div>';
 +
        echo '</article>';
 +
      }
 +
    ?>
 +
</syntaxhighlight>
 +
Sur chaque ligne du fichier JSON se trouvent les informations concernant une vidéo. Chacune de ces vidéos fera l'objet d'un "post" comme celui-ci :
 +
 
 +
[[Fichier:interface_v1_post.png|900px]]
 +
 
 +
Il contient des infos basiques (titre, durée, date à laquelle le bot l'a regardée).
 +
 
 +
Il faut désormais que je me penche sur la façon dont je vais analyser les métadonnées de chacune de ces vidéos pour les faire paraître dans l'interface.
 +
 
 +
Cependant toutes les métadonnées n'existent pas forcément pour chaque vidéo (certaines n'ont pas de tags par exemple) : il faut donc vérifier leur existence dans le tableau associatif crée à partir du log en JSON pour chaque vidéo. En PHP il y a [http://thinkofdev.com/php-fast-way-to-determine-a-key-elements-existance-in-an-array/ deux manières de faire ça], et je vais donc utiliser isset() puisque je n'aurais pas de cas où le contenu de l'array sea "null".
 +
<syntaxhighlight lang="php">
 +
  if (isset($JSON['snippet']['tags'])){
 +
    foreach($JSON['snippet']['tags'] as $tag){
 +
      echo '<p>'.$tag;'<p>';
 +
    }
 +
  }
 +
</syntaxhighlight>
 +
 
 +
J'ai fait un bout de code qui permet de comparer tous les tags dans les x derneiers éléments pour ressortir le plus récurrent et le poster dans le blog. Ça permet de se rendre compte périodiquement des récurrences ou des thématiques qui ressortent à l'échelle "locale" du blog.
 +
 
 +
<syntaxhighlight lang="php">
 +
  //s'il y a des tags, on les range dans une array pour les comparer
 +
        if (isset($JSON['snippet']['tags'])){
 +
          foreach ($JSON['snippet']['tags'] as $tag){
 +
            array_push($compare_tags, $tag);
 +
          }
 +
        }
 +
 
 +
        if ($compteur%4 == 3){
 +
          //compare les tags des dernières vidéos et en sort le plus récurrent
 +
          $compare_tags = array_map('strtolower', $compare_tags);
 +
          $compare_tags = array_count_values($compare_tags);
 +
          arsort($compare_tags);
 +
          $sorted_tags = array_keys($compare_tags);
 +
          $sorted_keys = array_keys(array_flip($compare_tags));
 +
          //affiche le tag le plus récurrent si il y a plus d'une occurence (et donc si c'est un minimum pertinent)
 +
          if ($sorted_keys[0] > 1){
 +
            echo '<article class="vid">';
 +
            echo 'En ce moment j\'aime bien '.$sorted_tags[0].' !';
 +
            echo '</article>';
 +
            //vide l'array, permet de comparer seulement les x derniers éléments
 +
            $compare_tags = [];
 +
          }
 +
        }
 +
        echo $compteur%4;
 +
        $compteur += 1;
 +
</syntaxhighlight>
 +
Cette structure est applicable à plusieurs métadonnées, avec des échelles différentes : on peut choisir de comparer les durées des vidéos, et d'en faire un compte-rendu avec une période de 20 vidéos, avec les catégories toutes les 30 vidéos, ou encore avec le nombre de likes ou de vues tous les 5 vidéos, etc.
 +
 
 +
Ces périodes peuvent être variables pour amener un peu de souplesse : on pourrait écrire ça comme ça:
 +
<syntaxhighlight lang="php">
 +
if ($compteur%15 == mt_rand(13, 15){
 +
//code   
 +
}
 +
</syntaxhighlight>
 +
La fonction mt_rand est apparemment [https://www.w3schools.com/PHP/func_math_mt_rand.asp plus rapide et "mieux aléatoire"] que la fonction rand().
 +
 
 +
Le problème est que ces posts ne sont pas dans l'ordre que je souhaiterais. J'ai utilisé une astuce dans la boucle en inversant l'ordre de l'array, c'est-à-dire que la dernière vidéo vue est postée en premier dans le code HTML, ce qui la place en haut de la page. Le problème, c'est que l'analyse comparative des tags par exemple se fait après dans cette même boucle, et donc si je décide de poster un commentaire du style "En ce moment j'aime bien x sujet", celui-ci se retrouvera en 3e position en partant du haut de la page. Le sens d'exécution du code/d'affichage est l'inverse du sens de logique de post sur un blog (les actualités les plus récentes en haut de la navigation.).
 +
 
 +
Je dois rectifier cet ordre soit par du javascript qui vient déplacer le noeud au bon endroit du DOM, soit peut-être par des paramètres de flexbox-order un peu compliqués pour que le noeud crée ''après'' soit positionné ''avant''.
 +
 
 +
En fait c'est très simple avec les [https://davidwalsh.name/css-reverse flexbox] ! Je peux enlever la ligne qui inverse l'ordre de l'array dans le code PHP.
 +
 
 +
Avec ce code CSS, les éléments générés dans l'ordre peuvent être affichés dans l'ordre inverse:
 +
<syntaxhighlight lang="CSS">
 +
/*Cet élément HTML est le conteneur de tous les posts, le changement d'ordre s'applique à tous ses enfants*/
 +
section#corps{
 +
  display:flex;
 +
  flex-direction: column-reverse;
 +
}
 +
</syntaxhighlight>
 +
 
 +
=====Affichage v2=====
 +
J'utilise des spans au lieu des divs pour faire en sorte que tous les messages se suivent.
 +
<syntaxhighlight lang="php">
 +
  foreach($arr_JSON as $JSON) {
 +
        echo '<div class="post hideable">';
 +
          //afficher la date
 +
          echo '<aside class="date">'.$JSON['datetime']['jour'].'</br>'.$JSON['datetime']['heure'].'</aside>';
 +
          echo '<p>';
 +
          echo '<span class="text">';
 +
            echo 'Je regarde une nouvelle vidéo. Le titre est <a href="https://www.youtube.com/watch?v='.$JSON['id'].'" class="titre" target="_blank">'.$JSON['snippet']['title'].'</a>.
 +
            Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'. </span> ';
 +
          echo '</span>';
 +
</syntaxhighlight>
 +
 
 +
Pour les images je crée une autre section :
 +
<syntaxhighlight lang="php">
 +
//on affiche les images dans une section différente donc on refait une boucle
 +
foreach($arr_JSON as $images_JSON) {
 +
  //trouver l'image dans la plus grande résolution possible et l'afficher
 +
  foreach ($thumb_res as $res) {
 +
    if (array_key_exists($res, $images_JSON['snippet']['thumbnails'])){
 +
      echo '<div>
 +
            <img class="hidden" src="'.$images_JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">
 +
            </div>';
 +
      break;
 +
    } else {
 +
      continue;
 +
    }
 +
  }
 +
}
 +
</syntaxhighlight>
 +
 
 +
Avec l'event "wheel" de javascript on peut récupérer n'importe quel mouvement de scroll, même s'il n'y a rien à scroller (alors que l'évènement "scroll" ne s'active que lorsque l'on doit défiler pour voir le reste du contenu).
 +
Le delta Y de cet évènement permet de savoir quand on va vers le haut ou vers le bas.
 +
<syntaxhighlight lang="javascript">
 +
imgs[0].classList.toggle('hidden');
 +
var number = 0;
 +
var prev_number = 0;
 +
window.addEventListener('wheel', function(event) {
 +
  if (event.deltaY < 0) {
 +
    //scrolling up
 +
    prev_number = number;
 +
    number = clamp(number - 1, 0, imgs.length - 1);
 +
  } else if (event.deltaY > 0) {
 +
    //scrolling down
 +
    prev_number = number;
 +
    number = clamp(number + 1, 0, imgs.length - 1);
 +
  }
 +
  console.log('number', number);
 +
  imgs[number].style.display = 'inherit';
 +
  if (number != prev_number) {
 +
    imgs[prev_number].style.display = 'none';
 +
  }
 +
});
 +
</syntaxhighlight>
 +
 
 +
Et la fonction clamp, qui permet de restreindre un nombre entre deux valeurs (pour éviter que des indexs trop grands ou trop petits créent une IndexError).
 +
<syntaxhighlight lang="javascript">
 +
function clamp(value, min, max) {
 +
  return Math.min(Math.max(value, min), max);
 +
}
 +
</syntaxhighlight>
 +
 
 +
J'essaie de compartimenter les différentes parties de l'interface (LIVE | TEXTE | IMAGES) en colonnes dont la taille peut être ajustée, afin de pouvoir se concentrer sur une partie en particulier par exemple. Pour cela j'utilise la librairie [https://github.com/nathancahill/split/tree/master/packages/splitjs#installation split.js].
 +
 
 +
PARTIE JS :
 +
<syntaxhighlight lang="javascript">
 +
//#live et #texte sont pour l'instant les deux colonnes que l'on peut retailler
 +
Split(['#live', '#texte'], {
 +
  sizes: [20, 80],
 +
  minSize: 100,
 +
  gutterSize:2,
 +
});
 +
</syntaxhighlight>
 +
 
 +
PARTIE CSS :
 +
Les colonnes retaillables ont la classe "split", et la gouttière ("gutter", "gutter-horizontal") est crée automatiquement entre les colonnes par la librairie. On peut changer la couleur ou mettre une image pour décorer cette gouttière.</br>
 +
Apparemment il faut que les colonnes aient une hauteur définie pour que la librairie fonctionne. Pour l'instant je leur ai donné une hauteur fixe car le reste ne fonctionnait pas.
 +
<syntaxhighlight lang="css">
 +
.gutter {
 +
    background-color: #eee;
 +
    height:100%;
 +
    top:4em;
 +
}
 +
 
 +
.gutter.gutter-horizontal {
 +
  float: left;
 +
  background-color: black;
 +
  cursor: ew-resize;
 +
}
 +
 
 +
.split, .gutter.gutter-horizontal {
 +
  height: 600px;
 +
  overflow-x: hidden;
 +
  overflow-y: auto;
 +
}
 +
</syntaxhighlight>
  
vid()
+
J'utilise ensuite AJAX pour récupérer le dernier log avec javascript, ce qui me permet d'utiliser les valeurs de l'objet JSON pour calculer le temps d'attente pour la prochaine vidéo.
 +
<syntaxhighlight lang="javascript">
 +
let requestURL = 'http://curlybraces.be/erg/2019-2020/portrait-robot/log_last.json';
 +
let request = new XMLHttpRequest();
 +
request.open('GET', requestURL);
 +
request.responseType = 'json';
 +
request.send();
  
 +
var logLast;
 +
request.onload = function() {
 +
  logLast = request.response;
 +
  document.getElementById('test').innerHTML = 'Prochaine vidéo dans '+logLast.contentDetails.duration;
 +
};
 
</syntaxhighlight>
 
</syntaxhighlight>

Version actuelle datée du 14 juin 2020 à 14:22

Scan to OCR

Comment une machine peut-elle reconnaître du texte ?

Trouver le pourcentage de pixels noir pour détecter les blocs de texte ? Traiter chaque bloc séparément pour trouver quel alphabet il faut essayer de détecter dans quel bloc ?

Comment une machine peut-elle "lire" une page ?

Si elle reconnaît les caractères, elle ne reconnaît pas les mots. Elle lirait donc les caractères un par un dans un bloc donné, puis passerait au suivant sans interruption. Il y aurait une sorte de condensation de tous les caractères dans une sorte de ligne infinie sans pause (comme le résultat de base que donne Tesseract : un fichier texte avec la chaîne de caractères reconnus sans interruption). C'est ce que la machine "peut" lire. Puisque la machine ne comprendrait pas non plus le sens des lettres (bien qu'un logiciel de text-to-speech reconnaît les mots et peut les lire) , (elle les connaît sous forme d'Unicode), elle pourrait aussi lire chaque "code" Unicode de chaque caractère à la suite. Dans le document que j'ai passé dans Tesseract il y a beaucoup de langues et d'alphabets différents, donc la variante Unicode serait plus "logique" (je ne sais pas si le text-to-speech peut gérer plusieurs langues en même temps. Ceci dit, cela fait partie de la façon dont une machine fonctionne, une fois qu'elle est lancée elle ne peut pas s'arrêter pour remarquer un changement, sauf si on le lui indique. Graphiquement cela pourrait se traduire par une grille de caractères dans une fonte monospace, chacun caractère étant dans un carré de mêmes dimensions. on aurait alors du texte "matriciel", fonctionnant case par case et compliqué à lire pour un humain, qui ne saurait plus où commencer et où arrêter les mots.


c o m p l i q u é à l i r e p o u r u n h u m a i n

Pour faire lire le texte par la machine avec un programme en python, on utilise le module pypiwin32 qui permet d'importer win32com (une bibliothèque de Windows ?). Il faut l'installer avec pip (Python Install Packages je crois) (attention aux histoires de windows 32 ou 64 bits et python 32 ou 64 bits, je crois que tout marche avec les versions 32 bits python).

import win32com.client as wincl
speak = wincl.Dispatch("SAPI.SpVoice")
speak.Speak(lecture)

Ce bout de script permet de lire le contenu de la variable lecture, qui est un chaîne de caractère tirée d'un fichier texte. On peut aussi écrire entre guillemets doubles les mots que l'on souhaite faire lire par le text-to-speech de Windows (je n'ai pas encore trouvé comment lire dans une langue différente ou avec une voix différente que la voix par défaut)


Je cherche le moyen de convertir chaque caractère en son code unicode (u220B par exemple) [1] -> lien vers les commandes unicode et encode en Python.

Je n'ai pas trouvé, donc j'ai simplement pu trouver une façon de convertir chaque caractère en hexadécimal, ce qui permet aussi aux text-to-speech de lire toutes les langues.

hex(ord('le caractère souhaité'))

Le code pour l'instant qui lit un fichier texte en hexadécimal :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from time import sleep

f = open("multi.txt", "r")

file = f.read()

lecture = ""

#transforme chaque caractère en
for i in range(0, len(file)):
    u = hex(ord(file[i]))
    print(u)
    #sleep(0.5)
    lecture = lecture + u + " "

#pour faire lire le texte par la voix de l'ordi (on peut sûrement la changer)
import win32com.client as wincl
speak = wincl.Dispatch("SAPI.SpVoice")
speak.Speak(lecture)

f.close()

Il manque maintenant un moyen de visualiser les caractères dans le terminal, en même temps qu'ils sont lus par le programme. Affaire à suivre sous Linux ?

Problème résolu : sous Linux le module pyttsx3 pour python 3 permet plein de réglages de paramètres de la voix (vitesse, volume, genre, langue...) et on peut lui dire d'attendre la fin de sa lecture pour continuer le programme. J'ai donc fait un script python qui affiche le contenu d'un fichier texte lettre par lettre (fonctionne en utf-8) et lit chaque caractère selon son code hexadécimal (ici en français mais on peut changer la voix).

Résultat sous Linux :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

f = open("multi.txt", "r")

file = f.read()

import pyttsx3
engine = pyttsx3.init()
voices = engine.getProperty('voices')
french_id = 'french'
engine.setProperty('voice', french_id)
engine.setProperty('rate', 150)

#transforme chaque caractère en hexadécimal
# + fait lire le texte par la voix de l'ordi (on peut sûrement la changer) mais affiche le caractère d'origine
for i in range(0, len(file)):
    u = hex(ord(file[i]))
    print(file[i])
    engine.say(u)
    engine.runAndWait()

f.close()

Traiter des fichiers avec Tesseract

Test du jour : utiliser Tesseract pour détecter toutes les langues en même temps Utiliser Google traduction pour trouver quel bloc (quel alphabet) appartient à quelle langue, pour pouvoir utiliser le bon code dans Tesseract [2] : le Github de Tesseract avec tous les codes de langues prises en charge par Tesseract.

La page que j'ai scannée est un mode d'emploi/conseils d'utilisation d'un matelas IKEA, avec beaucoup d'alphabets différents en simultané :

Test1.jpg
Test2.jpg


résultat de tesseract :

<!doctype html>
<html>
  <head>
  </head>
  <body>

Madraci od pjene i lateksa

Prije prve upotrebe

Možda se isprva madrac može učiniti

malo pretvrdim. Bit će potrebno mjesec
dana da se tijelo navikne na madrac i da
se madrac prilagodi tijelu. Za najveću
udobnost potreban je odgovarajući jastuk.
Zato je dobro imati jastuk koji odgovara
načinu spavanja i novom madracu. Neki
od madraca su rolo pakirani. Iako se mogu
odmah koristiti, svoj će pravi oblik, duljinu
i debljinu vratiti nakon 3 — 4 dana. Svi novi
materijali imaju poseban miris, ali on će
nakon nekog vremena nestati. To se može
i ubrzati prozračivanjem i usisavanjem
madraca.

Njega i čišćenje

Uz madrac je dobro koristiti zaštitu za
madrac. Ona je više higijenska jer se

lako skida i čisti. Puno naših madraca

ima navlaku koja se može prati. Za više
informacija potrebno je pročitati etiketu s
unutarnje strane navlake. Prilikom pranja
navlake za madrac potrebno je zatvoriti
patent-zatvarač. Usisavanjem madraca
uklanjaju se prašina i grinje. Madrac se

ne smije preklapati jer se tako može
oštetiti njegova unutrašnjost. Cak i oni
najbolji madraci s vremenom postaju
manje udobnima i svi madraci s vremenom
nakupljaju prašinu i grinje. Zato predlažemo
promjenu madraca svakih 8 - 10 godina.

中 文
泡沫 和 乳胶 床 垫

首次 使 用 前

起 初 , 您 的 新 床 墊 可 能 感覺 有 點 硬 。 要 有 一 個 月 的

时 间 ,让 您 的 身体 习惯 您 的 新 床 垫 ,您 的 新 床 垫 也

习惯 您 的 身体 . 为 了 获得 最 佳 的 舒适 度 , 您 需要 一
款 合 适 的 枕头 。 请 确保 您 拥有 一 款 适 合 您 和 您 床 垫
的 枕头 。 包装 成 卷 的 床 垫 在 使 用 3、4 天 后 会 恢复 原
状 。 所 有 新 才 有 都 有 各 自 的 特殊 气味 , 但 是 该 气味

จ 了 晾晒 床 垫 ,使 其 保持 通风 ,有 助 于 消

保养 和 清洁

将 床 垫 与 床 垫 保护 垫 并 用 . 这 样 ,更 清洁 卫生 , 因
为 床 垫 保护 垫 易 于 取 下 清洗 .。 我 们 的 许多 床 垫 都 有
可 拆洗 床 垫 套 . 可 阅读 床 垫 套 内 的 标签 了 解 更 多 详
情 . 洗涤 床 垫 套 时 ,请 确保 拉链 已 拉 合 . 对 床 垫 吸 尘
有 助 于 清楚 灰尘 和 螨虫 。 请 不 要 折 赤 床 垫 。 这 可 能
损坏 内 部 材料 。 即使 最 好 的 床 垫 也 会 随 着 岁月 的 流
逝 舒 适度 逐渐 降低 ,多 年 之 后 ,所 有 的 床 垫 都 会 积
聚 灰 和 侍 和 螨虫 . 因此 ,我 们 还 是 建议 您 在 8-10 年 后
更 换 您 的 床 垫 。

ไท ย
ท ี ่ น อ น โฟ ม แล ะ ท ี ่ น อ น ย า ง ал ІЙ пої

ข้ อ แน ะ น ํ า ก ่ อ น ก า ร ใช ้ ง า น ค ร ั ้ ง แร ก

โด ย ท ั ่ ว ไป แล ้ ว ท ี ่ น อ น ให ม ่ จ ะ แน ่ น เป ็ น พ ิ เศ ษ จ ึ ง ต ้ อ ง ใช ้
เว ล า ป ร ะ ม า ณ ห น ึ ่ ง เด ื อ น เพ ื ่ อ ให ้ ร ่ า ง ก า ย เค ย ชิ น ก ั บ ท ี ่ น อ น
ให ม ่ แล ะ ท ี ่ น อ น ป ร ั บ เข ้ า ก ั บ ร ่ า ง ก า ย ค ุ ณ น อ ก จ า ก ท ี ่ น อ น
ด ี ๆ แล ้ ว ห ม อ น เป ็ น อ ี ก ป ั จ จ ั ย ท ี ่ ท ํ า ให ้ ค ุ ณ น อ น ห ล ั บ ส บ า ย
ย ิ ง ขึ ้ น เล ื อ ก ห ม อ น ท ี ่ เห ม า ะ ก ั บ ค ุ ณ แ ล ะ ท ี ่ น อ น ให ม ่ ท ี ่ น อ น
บ า ง ร ุ ่ น ม ้ ว น บ ร ร จ ุ ใน ห ุ ่ อ พ ล า ส ต ิ ก แม ้ ส า ม า ร ถ แ ก ะ ใช ้ ได ้
ท ั น ท ี แต ่ ต ้ อ ง ใช ้ เว ล า 3-4 ว ั น ใน ก า ร ค ื น ร ู ป ท ร ง . ท ี ่ น อ น
ให ม ่ ม ั ก ม ี ก ล ิ น เฉ พ า ะ ขอ ง ว ั ส ด ุ ก า ร น ํ า ท ี ่ น อ น ไป ผึ่ง แล ะ ด ู ด
ฝุ ่ น ส า ม า ร ถ ข จ ั ด ก ล ิ น ได ้ เร ็ ว ขึ น

ข้ อ แน ะ น ํ า ใน ก า ร ด ู แล ร ั ก ษา แล ะ ท ํ า ค ว า ม ส ะ อ า ด

เล ื อ ก ใช ้ ผ้า ร อ ง ก ั น เป ื ้ อ น ท ี ่ น อ น เพ ื ่ อ ให ้ ท ี ่ น อ น ส ะ อ า ด แล ะ

ถู ก ส ุ ขอ น า ม ั ย ย ิ ง ขึ ้ น เพ ร า ะ ถอด ซั ก ท ํ า ค ว า ม ส ะ อ า ด ได ้ ง ่ า ย
ท ิ น อ น บ า ง ร ุ ่ น ถอด ผ้า ห ุ ้ ม อ อ ก ซั ก ได ้ ก ร ุ ณา อ ่ า น ข้ อ แน ะ น ํ า

ใน ก า ร ด ู แล ร ั ก ษา ท ี ่ น อ น จ า ก ป ้ า ย ส ิ น ค ้ า แล ะ ก ่ อ น ซั ก ผ้า ห ุ ้ ม
ท ี ่ น อ น ค ว ร ร ู ด ซิ ป ป ิ ด ผ้า ห ุ ้ ม ท ุ ก ค ร ั ้ ง ค ว ร ด ู ด ฝุ ่ น ท ี ่ น อ น เพ ื ่ อ

ก ํ า จ ั ด ไร ฝุ ่ น แล ะ ใช ้ น ํ า ย า ท ํ า ค ว า ม ส ะ อ า ด เบ า ะ เช ็ ด ค ร า บ

ส ก ป ร ก ไม ่ ค ว ร พ ั บ ท ี ่ น อ น เพ ร า ะ อ า จ ท ํ า ให ้ ส ป ร ิ ง แล ะ ว ั ส ด ุ

ภา ย ใน เส ี ย ห า ย ได ้ แต ่ ไม ่ ว ่ า ท ี ่ น อ น จ ะ ด ี เพ ี ย ง ใด เม ื ่ อ เว ล า

ผ่ า น ไป ก ็ อ า จ เส ื ่ อ ม ค ุ ณ ภ า พ ล ง ได ้ ต า ม อ า ย ุ ก า ร ใช ้ ง า น ท ั ้ ง
ย ั ง เป ็ น แห ล ่ ง ส ะ ส ม ไร ฝุ ่ น อ ี ก ด ้ ว ย จ ึ ง ค ว ร เป ล ี ่ ย น ท ี ่ น อ น ให ม ่
ท ุ ก ๆ 8-10 ป ี

Στρώματα Αφροῦΐῦ και Λάτεξ

Πριν απὸ την πρώτη χρήση

Στὴν арх! το καινούργιο σας ชา อ อ น ส

μπορεὶ να σας φαίνεται λίγο σκληρό. Δῶστε
περιθώριο περίπου ἑνα μήνα στο σὧμα σας

να συνηθίσει το στρώμα Και στο στρώμα να
προσαρμοστεὶ στο σώμα σας. Για να ἐχετε

τη μεγαλύτερη δυνατόν ἀνεση, Χρειάζεστε

το σωστὸ μαξιλάρι. Βεβαιωθείτε OTI ἐχετε

ἐνα μαξιλάρι που ταιριάζει σε εσάς ка! то
καινούργιο σας στρώμα. Μερικὰ απὸ τα
στρώματὰ μας εἶναι συσκευασμένα σε ρολό.
Μποροὺν να χρησιμοποιηθοὺν αμέσως, αλλά
θα ἐεπανακτήσουν το πλήρες σχήμα, μήκος Καὶ
πάχος μετά απὸ 3-4 ημέρες. Ὁλα τα καινούργια
и№Мка ἐχουν τη δική τους Ιδιαίτερη μυρωδιά,
η οποία σταδιακἀ εξαφανίζεται. Αερίζοντας
και καθαρίζοντας το στρώμα με την ηλεκτρικὴ
σκούπα, βοηθὰ στην εξάλειψη της μυρωδιάς.

Φροντίδα και καθάρισμα

Συμπληρώνοντας Το στρώμα σας με ἕνα
προστατευτικὸ στρώματος ἡ ανώστρωμα θα

το διατηρείτε πιο υγιεινό, καθὼς εἶναι εὐκολο
να το αφαιρέσετε και να το πλύνετε. Πολλά
από τα στρὡματά μας διαθέτουν κάλυμμα που
πλένεται. Διαβάστε το ταμπελάκι στο εσωτερικό
του καλύμματος για οδηγίες πλυσίματος.
Βεβαιωθείτε ὁτι το φερμουάρ εἶναι κλειστό
προτοὺ πλύνετε το κάλυμμα. Το σκούπισμα με
την ηλεκτρικὴ σκούπα βοηθά να αφαιρέσετε

τη σκόνη και Τα ακάρεα. Μη διπλώνετε Το
στρώμα, καθὼς μπορεὶ να φθείρει τα υλικά στο
εσωτερικό. Акора ка! та καλύτερα στρώματα
γίνονται λιγότερο ἀνετα με το πέρασμα του
χρόνου και συσσωρεύουν σκόνη και ακάρεα. ΓΙ’
ауто OQGC OUVIOTOUHUE VQ AAQCETE OTDQOUQ KaBE
8-10 xpovia.

每 中
泡 棉 / 乳 膠 床 墊

第 一 次 使用 前

新 床 墊 剛 使 用 時 感覺 可 能 偏 硬 ' 建議 至 少 給 你 的 身體
2 到 4 週 的 時 間 去 適應 新 床 墊 , 同 時 也 讓 新 床 墊 能 逐
漸 貼 合 你 的 身體 曲線 。 要 擁有 最 佳 舒 適 感 , 你 還 需要
選擇 對 的 枕頭 , 別 忘 了 確認 你 的 枕頭 適合 你 及 你 的 新
床 墊 ‧ 部 份 床 墊 採 捆 捲 式 包裝 , 使用 3-4 天 後 即 可 恢
复 既 有 长 度 及 厚度 。 初 次 打开 包装 可 能 会 闻 到 特殊 气
味 , 此 味道 將 隨時 間 逐 漸 消散 。 請 放心 使用 ! 這些 氣
味 經 實驗 證明 對 人 體 無 害 。 只 要 置 於 通風 良好 之 環
境 ' 並 使用 吸塵 器 即 可 有 效 降低 此 氣味 。

保養 與 清潔

在 床 墊 上 添加 保潔 墊 , 能 保持 床 墊 衛生 且 方便 清洗 。
多 款 床 墊 含 可 拆洗 式 布 套 ' 在 清洗 前 別 忘 了 參閱 布 套
內 側 的 清洗 標籤 ‧ 清 洗 時 請 記得 拉 上 拉鍊 。 建 議 使用
吸塵 器 清潔 床 墊 , 可 有 效 清 除 灰 塵 與 塵 虹 ‧ 請 勿 曲折
床 墊 , 這 麼 做 會 導致 床 墊 的 內 部 結構 受 損 。 經 過 長 時
間 的 使用 . 再 好 的 床 墊 舒適 度 也 會 遞減 , 灰 塵 與 塵 蝶
也 會 逐漸 累積 因此 ,. 基於 舒適 與 衛生 考量 , 我 们 建
議 每 隔 8 到 10 年 更 換 新 的 床 墊 。

Матрасы из пенополиуретана и
латекса

Перед началом использования
Первое время матрас может показаться
вам слишком жестким. Чтобы
привыкнуть к новому матрасу, вам
может потребоваться около месяца.

Оптимальный комфорт обеспечит

правильно подобранная подушка.
Некоторые матрасы из нашего
ассортимента упакованы в рулон. Такой
матрас можно использовать сразу после
покупки, но полностью он восстановит
свою форму, длину и толщину через
3-4 дня использования. Материал
обладает характерным запахом, который
постепенно исчезнет. Проветривайте и
пылесосьте матрас, чтобы запах исчез
полностью.

Уход

Дополните матрас наматрасником,
который легко снять и постирать. Многие
матрасы из ассортимента ИКЕА продаются
со съемными чехлами, которые можно
стирать. Подробная информация по
уходу представлена на этикетке внутри
чехла. Во время стирки молния на чехле
должна быть закрыта. Чистка пылесосом
удаляет пыль и пылевых клещей. Не
складывайте матрас, это может повредить
его. Даже самый хороший матрас со
временем становится менее удобным, в
нем скапливается пыль и пылевые клещи.
Поэтому мы рекомендуем менять матрас
каждые 8-10 лет.

한국어
폼 매 트 리 스 와 라텍스 매트리스

사용 전

처 음 에는 다소 딱 딱 하게 여 겨 질 수도 있으나 한 달
정도 사 용 하 면 내 몸 에 맞는 편안한 매 트 리 스 가
됩니다. 더 안락한 잠 자 리 를 위해 나 에게 꼭 맞는
베 개 도 준비해 보세요. 일부 매 트 리 스 는 말아서
포 장 되어 구입 직후 사용할 수 는 있으나 제품

원 형 으로 복 원 되 는데 3~4 일 정 도 의 시 간 이

소 요 됩니다. 제 품 에 배 어 있 는 냄 새 는 시 간 이 지나면
자연스럽게 사 라 집니다. 진 공 정 소 기로 흡 입 하 고
바 람 이 잘 통하는 곳에 두면 냄새 제 거 에 도 움 이
됩니다.

관 리 와 세척

매트리스 보 호 커 버 를 사 용 하면 세 탁 이 가 능 하 므로
침 대 를 더 깨끗이 사용할 수 있습니다. 대 부 분 의
커 버 는 물 세 탁 이 가 능 합니다. 더 자세한 내 용 은
커버 안 쪽 의 표 를 참 고 하세요. 매트리스 커버

세 탁 시 지 퍼 는 반드시 채 워 주 세요. 진 공 정 소 기로
매 트 리 스 의 먼 지 와 진 드 기 를 제 거 할 수 있습니다.
제 품 에 손 상 이 갈 수 있으니 접어서 보 관 하지
마세요. 최고 품 질 의 매 트 리 스 도 사 용 할 수록

안 락 함 이 줄어들고 먼 지 와 진 드 기 가 생기게 됩니다.
68~10 년 에 한 번 정도 매 트 리 스 를 교 체 하 는 것이

좋습니다.

Матраци із пінополіуретану та
латексу

Перед використанням вперше
Спочатку новий матрац може здаватися
надто твердим. Потрібен хоча б один
місяць, щоб ваше тіло звикло до

матраца, а матрац набув форми тіла.

Для найбільшого комфорту вам потрібна
правильна подушка. Підберіть подушку,
яка підходить для вашого нового матраца.
Деякі матраци постачаються у згорнутому
вигляді. Їх можна використовувати
відразу, але вони відновлять свою форму,
довжину та товщину лише через 3-4

дні. Усім новим матеріалам властивий
особливий запах, який поступово зникає.
Можна провітрити та пропилососити
матрац, щоб позбутися запаху.

Догляд та очищення

Доповніть матрац спеціальним захисним
чохлом, який легко знімати та чистити.
Багато матраців постачаються з

чохлом, який можна прати. Додаткову
інформацію наведено на ярлику всередині
чохла. Перед пранням чохла матраца
переконайтеся, що блискавка застібнута.
Можна пропилососити матрац, щоб
видалити пил і кліщів. Не згинайте
матрац. Це може пошкодити матеріал
наповнювача. Із часом навіть найкращі
матраци втрачають зручність, і в усіх
матрацах збираються пил та кліщі. Саме
тому ми рекомендуємо міняти матрац
кожні 8-10 років.

日 本 語
フォ ー ム マッ トレ ス & ラ テッ クス マッ トレ ス

初め て ご 使用 に な る 前 に

初め は 少し 硬め に 感じ られ る か も し れ ま せん が 、1
力 月 ほど で マッ トレ ス が 体 に な じん で きま す 。 A
適 に 眠る た め に は 、 自 分 に 合っ た 枕 選 び が 大 切 で
す 。 マ ッ ト レ ス を 替え た と き は 、 枕 も 見 直し まし
よう 。 イ ケア の マッ トレス の な か に は ロー ル パ ッ
ク に な っ た も の も あり ます 。 開封 後 す ぐに 使用 で
きま す が 、 本 来 の 形 ・ 厚 さ に 戻る まで に 3、4 日
ほど か か り ま す 。 最初 は 新品 特有 の (におい が し ま
す が 、 し だ い に 消 えて いき ます 。 マ ッ ト レ ス を 風
に あて れ ば 、 ご の に お い を 早く 消す こと が で きま
3.

お 手入れ 方 法

マッ トレ ス の 上 に マッ トレ スプ ロ テ ク ター を 敷く
こと を お すす めし ます 。 マ ッ トレス プロ テク ター
は 手軽 に 取り 外し て 洗え る の で 、 マ ッ トレ ス を い
つも 清潔 に 保 て こま す 。 イ ケア の マッ トレ ス の 多く
は 、 カ バー を 取り 外し て 洗え ます 。 洗濯 方 法 は 、
カバ ー に 付い て いる タグ を ご 覧 くだ さい 。 カ バー
を 洗濯 する と き は 、 必 ず フ ァ ス ナー を 閉じ て くだ
さい 。 ホ コリ や ダニ の 除去 に は 、 掃 除 機 を か ける
の が 効果 的 で す 。 マ ッ ト レ ス を 曲げ な いで くだ さ
い 。 マットレス を 曲げ る と 、 内 部 の 素材 に 損傷 を
与え る お それ が あり ます 。 高 品質 の マッ トレ ス

で も 、 長 年 使用 する と 快適 さ が 損 な われ 、 ホ コリ
や ダニ が た まり や すく な り ま す 。 快 適 で 衛生 的 に
お 使い いた だ く た め に 、 イ ケア で は 8 ぐ <  10   
 マッ トレ   交換 する こと   すす めし  いま

o


cat 1.txt 2.txt  3.txt 4.txt > final.txt

Combine tous les textes en un seul.

cat final.txt | shuf > final_mélangé.txt

Mélange toutes les lignes.

Ensuite on peut garder les 20 premières lignes seulement, ce qui permettrait d'avoir , avec un peu de chance, au moins une ligne de chaque texte. Créer un script python qui compte le nombre de syllabes pour faire des haikus ? (ou faire des alexandrins ?) -> [3]

Balisage des pages : Print Party

Session balisage des fichiers textes sortis par Tesseract (en HTML5). J'ai balisé en séparant les différentes langues en sections, et dans chaque section on a le même schéma : le nom de la langue en capitales en h1, les sous-titres en gras en h2 et les paragraphes de textes. J'essaie de trouver un moyen de baliser les plis qui ont disparu au scan, afin de pouvoir m'en servir plus tard (peut être pour créer un truc interactif avec javascript, pour pouvoir superposer les différents textes dans la même langue, puisque mes 2 scans se chevauchent).

J'ai découvert que, dans Tesseract, les textes dans d'autres langues à proximité d'un texte donné modifient la reconnaissance de ce texte. Plus il y a d'alphabets cohabitant en jeu, plus il y a de risque de modification de la reconnaissance optique du texte donné. La documentation de Tesseract explique également que la même image donnera deux résultats différents selon l'ordre dans lequel on inscrit les langues (-l eng+deu =/= -l deu+eng)

J'aimerais créer une page html qui permet de sélectionner une langue pour comparer les résultats d'OCR avec toutes les autres, ou 4 par 4 ou 2 par 2 etc. Pour cela j'enregistre chaque bloc de texte dans un fichier de taille et de résolution fixe, en le nommant avec le nom de la langue qu'il contient. Le programme utiliserait PHP pour appeler Image Magick pour coller les images ensembles, enregistrer le fichier, appliquer une reconnaissance de Tesseract en fonction des langues sélectionnées. Il stockerait le résultat dans des variables pour pouvoir l'afficher mis en page au côté des autres versions du même texte d'origine.

Avec php on peut lancer des commandes shell (c'est un peu lent mais ça va) :

  • shell_exec lance une commande shell
  • magick correspond à une commande ImageMagick (une bibliothèque shell ? existe un moyen d'en faire une bibliothèque php je crois)
  • montage assemble des images
  • -background #FFFFFF met la couleur de fond en blanc
  • -geometry rajoute une petite marge
shell_exec("magick montage -background #FFFFFF -geometry +4+4 ".$img_base." ".$img2." img/montages/".$i.".jpg");

Ensuite il faut utiliser shell_exec pour appliquer tesseract à ces images. J'ai enregistré toutes les combinaisons possibles et j'ai supprimé dans chaque fichier texte la moitié du texte qui ne correspondait pas à la langue voulue.

Le programme permet de choisir la langue que l'on veut comparer, et affiche dans un programme le résultat sous forme de poster imprimable.

Tur.jpg

(exemple de poster imprimable pour l'instant)

Le programme est assez lent et ce n'est pas pratique de générer à chaque fois tout le bazar, alors j'ai juste généré ça en off et le programme va chercher les infos sur mon ordi.

J'ai utilisé la règle css

writing-mode: vertical-rl;
text-orientation: upright;

pour que le texte s'affiche en vertical, afin de pouvoir comparer ligne par ligne la différence entre les différentes itérations de l'OCR. Pour cela j'ai du régler l'interlignage (qui est en fait l'interlettrage en vertical avec

 letter-spacing: -2rem;

.

Je voulais donc une police monospaced, pour avoir cet effet de "grille" pour que l'on puise comparer les lignes. J'ai eu un problème pour trouver une police qui comprenne le plus d'alphabets possibles, pour garder une cohérence dans le poster. La police IBM Plex est celle que j'ai trouvée avec le plus de caractères, et avec une version thai et arabe. Les seuls alphabets non-pris en compte sont les alphabets grecs et chinois.

Apparemment il existe une Nimbus Mono Global qui inclut tous les alphabets dont j'ai besoin mais (mais elle coûte 1950€).

Maintenant j'essaie d'améliorer la clarté du poster en indiquant au-dessus de chaque colonne la langue avec laquelle la langue sélectionnée a été scannée pour donner ce résultat. Problèmes de CSS.

J'ai réussi à faire ce que je voulais mais c'est pas très clair. On ne se rend pas vraiment compte de la découverte que j'ai faite sur tesseract : le fait qu'une même langue, scannée à côté de différentes autres langues, ne donne pas le même résultat. Un texte en chinois scanné près d'un texte en arabe n'aura pas le même résultat que ce même texte en chinois scanné près de texte en russe par exemple.

Il faut désormais qu'il faut que je trouve une mise en page plus simple qui permette de se rendre compte de ça. Peut-être juste des extraits scannés deux par deux ? Choisir une langue de référence (le français peut-être, qu'on puisse lire), pour générer un certain nombre de posters. que l'on puisse comparer le même texte chamboulé par son scan près d'autres langues.

J'ai trouvé la police Noto (qui est par défaut sur Debian 10 en fait) de Google, une linéale qui possède tous les alphabets du monde. Elle est disponible dans plein de styles différents.

Suite du projet

J'ai changé complètement de mise en page, pour être plus clair dans mon intention. Ce que je veux montrer c'est la comparaison entre les résultats de Tesseract sur le même texte avec différents réglages de langues. Pour ça je conserve les deux premiers paragraphes du texte, et je re-génère avec Imagemagick et Tesseract toutes les comparaisons possibles avec l'extrait en thaïlandais. Sur chaque version du poster, il y aura le texte d'origine en thaïlandais, le texte avec lequel il sera comparé (dans une autre langue), et le résultat de Tesseract de l'interprétation du texte en thaïlandais quand les deux langues en question sont dans les paramètres.

Exemples de résultats:

Poster nor.jpg

Affiche pour le Rideau de Perles

Pour l'ouverture du Rideau de Perles, j'ai fait un "générateur" d'affiches. Par-dessus les posters j'ai rajouté le mot "ouverture" dans toutes les 9 langues du mode d'emploi que j'ai sélectionnées. Tous ces mots en surimpression sont semi-transparents, sauf "ouverture" en français et celui dans la langue correspondant au poster en arrière-plan.

Exemples :

Poster grc.jpg
Poster ara.jpg

Les affiches sont générées avec PHP/HTML/CSS, et le changement d'opacité est géré par JavaScript. J'ai fait une version web qui alterne entre toutes Le résultat est visible sur le site [rideaudeperles.be], avec les autres propositions du cours (recharger la page plusieurs fois pour voir les autres versions, qui sont choisies aléatoirement à chaque chargement de la page).

Postergif.gif

Code

PHP

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <link href="affiche3_print.css" type="text/css" rel="stylesheet" media="all">
    <!--<link href=affiche3_web.css type=text/css rel=stylesheet media=screen>-->

    <?php
      ini_set('display_errors', 1);
      ini_set('display_startup_errors', 1);
      error_reporting(E_ALL);
      ini_set('max_execution_time', 300);

      include 'variables.php';
     ?>

  </head>

  <body>
    <div class="hidden">
      <div id="pur_ara" > <?php echo  htmlspecialchars($pur_ara); ?> </div>
      <div id="pur_bel" > <?php echo htmlspecialchars($pur_bel); ?> </div>
      <div id="pur_chi_sim" > <?php echo htmlspecialchars($pur_chi_sim); ?> </div>
      <div id="pur_grc" > <?php echo htmlspecialchars($pur_grc); ?> </div>
      <div id="pur_hrv" > <?php echo htmlspecialchars($pur_hrv); ?> </div>
      <div id="pur_nor" > <?php echo htmlspecialchars($pur_nor); ?> </div>
      <div id="pur_pol" > <?php echo htmlspecialchars($pur_pol); ?> </div>
      <div id="pur_tha" > <?php echo htmlspecialchars($pur_tha); ?> </div>
      <div id="pur_tur" > <?php echo htmlspecialchars($pur_tur); ?> </div>

      <div id="ara_tha" > <?php echo htmlspecialchars($ara_tha); ?> </div>
      <div id="bel_tha" > <?php echo htmlspecialchars($bel_tha); ?> </div>
      <div id="chi_sim_tha" > <?php echo htmlspecialchars($chi_sim_tha); ?> </div>
      <div id="grc_tha" > <?php echo htmlspecialchars($grc_tha); ?> </div>
      <div id="hrv_tha" > <?php echo htmlspecialchars($hrv_tha); ?> </div>
      <div id="nor_tha" > <?php echo htmlspecialchars($nor_tha); ?> </div>
      <div id="pol_tha" > <?php echo htmlspecialchars($pol_tha); ?> </div>
      <div id="tur_tha" > <?php echo htmlspecialchars($tur_tha); ?> </div>
      <div id="tha_tha" > <?php echo htmlspecialchars($tha_tha); ?> </div>
    </div>

  <section class="page">
    <div class ="hidden" id="rand"></div>
    <section class="source" id="txt_origine">
      <div class="wrapper">
        <?php echo $pur_tha; ?>
      </div>
    </section>

    <div class="plus"><p>+</p></div>

    <section class="source" id="txt_côté">
      <div class="wrapper" id='côté'></div>
    </section>

    <div class="langue">
      <div id="lang"></div>
    </div>

    <section id="txt_ocr"></section>

    <section class="info_karaoké" id="karaoké">
      <div class="opacity" id='K_ara'>افتتاح</div>
      <div class="opacity" id='K_bel'>АДКРЫЦЦЁ</div>
      <div class="opacity" id='K_chi_sim'>开幕</div>
      <div class="opacity" id='K_grc'>ΕΓΚΑΙΝΙΑ</div>
      <div class="opacity" id='K_hrv'>OTVARANJE</div>
      <div class="opacity" id='K_fra'>OUVERTURE</div>
      <div class="opacity" id='K_nor'>ÅdivNING</div>
      <div class="opacity" id='K_pol'>OTWARCIE</div>
      <div class="opacity" id='K_tha'>เปิด</div>
      <div class="opacity" id='K_tur'>AÇILIŞ</div>
    </section>

  </section>

  <script type=text/javascript src=gif.js></script>

  </body>

</html>

CSS

p, h1, h2, h3, h4, h5, h6, footer, header, section, article, aside{
margin:0;
padding:0;
font-weight:normal;
}

@media print{
	section.page{
    overflow: hidden;
		margin:0;
  }
  head{
  display: none;
  }
}

@page{
size:297mm 420mm;
margin:0;
/*permet de forcer le navigateur à choisir autimatiquement le bon format pour imprimer*/
}

html, body {
margin:0;
padding:0;
font-size: 10pt;
background-color: black;
font-family: monospace;
overflow: hidden;
position: relative;
}

header{
	height: 10mm;
	margin-top: 5mm;
}

section.page{
  background-color: white;
  box-sizing: border-box;
  height: 420mm;
  width: 297mm;
  padding: 0mm;
	margin-left: auto;
  margin-right: auto;
  white-space: normal;
  overflow: hidden;
  position: relative;
  text-overflow: ellipsis;
  }

p{
	width: 9%;
	text-align: center;
	font-family: 'IBM Plex Mono';
	position: relative;
	float: left;
	margin-right: 0.8rem;
	display:inline-block;
}

div.ocr{
	position: relative;
	width: 100%;
	margin-top: 5mm;
	text-align: center;

}

.texte{
	font-size: 5.5rem;
	letter-spacing: -0.5rem;
	writing-mode: vertical-rl;
	text-orientation: upright;
}

.langue{
	font-size: 3rem;
	transform: translate(4pt);
}

#clear{
	clear:both;
}

@font-face {
font-family: 'IBMPlexMono';
src: url('fonts/IBMPlexMono-SemiBold.otf');
}

@font-face {
font-family: 'IBMPlexArabic';
src: url('fonts/IBMPlexArabic-SemiBold.otf');
}

@font-face {
font-family: 'IBMPlexSansThai';
src: url('fonts/IBMPlexSansThai-SemiBold.otf');
}

@font-face {
font-family: 'Ubuntu';
src: url('fonts/UbuntuMono-B.ttf');
}

@font-face {
font-family: 'WenQuanYi';
src: url('fonts/WenQuanYi Micro Hei Mono.ttf');
}

#ara{
	font-family: 'IBMPlexArabic';
}

#tha{
  font-family: 'IBMPlexSansThai';
}

#grc{
	font-family: 'Ubuntu';
}


JavaScript

var ara_tha = document.getElementById("ara_tha");
ara_tha = ara_tha.textContent;
var bel_tha = document.getElementById("bel_tha");
bel_tha = bel_tha.textContent;
var chi_sim_tha = document.getElementById("chi_sim_tha");
chi_sim_tha = chi_sim_tha.textContent;
var grc_tha = document.getElementById("grc_tha");
grc_tha = grc_tha.textContent;
var hrv_tha = document.getElementById("hrv_tha");
hrv_tha = hrv_tha.textContent;
var nor_tha = document.getElementById("nor_tha");
nor_tha = nor_tha.textContent;
var pol_tha = document.getElementById("pol_tha");
pol_tha = pol_tha.textContent;
var tha_tha = document.getElementById("tha_tha");
tha_tha = tha_tha.textContent;
var tur_tha = document.getElementById("tur_tha");
tur_tha = tur_tha.textContent;

var pur_ara = document.getElementById("pur_ara");
pur_ara = pur_ara.textContent;
var pur_bel = document.getElementById("pur_bel");
pur_bel = pur_bel.textContent;
var pur_chi_sim = document.getElementById("pur_chi_sim");
pur_chi_sim = pur_chi_sim.textContent;
var pur_grc = document.getElementById("pur_grc");
pur_grc = pur_grc.textContent;
var pur_hrv = document.getElementById("pur_hrv");
pur_hrv = pur_hrv.textContent;
var pur_nor = document.getElementById("pur_nor");
pur_nor = pur_nor.textContent;
var pur_pol = document.getElementById("pur_pol");
pur_pol = pur_pol.textContent;
var pur_tha = document.getElementById("pur_tha");
pur_tha = pur_tha.textContent;
var pur_tur = document.getElementById("pur_tur");
pur_tur = pur_tur.textContent;

var l_tha = [ara_tha, bel_tha, chi_sim_tha, grc_tha, hrv_tha, nor_tha, pol_tha, tha_tha, tur_tha];
var l_pur = [pur_ara, pur_bel, pur_chi_sim, pur_grc, pur_hrv, pur_nor, pur_pol, pur_tha, pur_tur];
var l_lg = ['ARABE', 'BIÉLORUSSE', 'CHINOIS<br>SIMPLIFIÉ', 'GREC', 'CROATE', 'NORVÉGIEN', 'POLONAIS', 'THAÏLANDAIS', 'TURC'];
var langues = ['K_ara', 'K_bel', 'K_chi_sim', 'K_grc', 'K_hrv', 'K_nor', 'K_pol', 'K_tha', 'K_tur'];

window.onload = function start() {
  timer();
  setInterval(timer, 500);
}
var i = 0;

//la fonction timer() utilise un compteur qui défile externe pour synchroniser
//le clignotement et le changement de texte
function timer(){
  i = i%9;
  document.getElementById("rand").innerHTML = i;
  var lang = document.getElementById(langues[i]);
  var w_tha = document.getElementById("txt_ocr");
  var pur = document.getElementById("côté");
  var lg = document.getElementById("lang");

  lang.style.opacity = '1';
  w_tha.innerHTML = l_tha[i];
  pur.innerHTML = l_pur[i];
  lg.innerHTML = l_lg[i];
  w_tha.style.fontFamily = "NotoMed";
  pur.style.fontFamily = "NotoMed";

  if (i==0){
    document.getElementById(langues[8]).style.opacity = '0.5';
    w_tha.style.fontFamily = "NotoAra";
    pur.style.fontFamily = "NotoAra";

  } else if (i==2){
    document.getElementById(langues[i-1]).style.opacity = '0.5';
    w_tha.style.fontFamily = "NotoMed";
    pur.style.fontFamily = "NotoMed";

  } else if (i==7){
    document.getElementById(langues[i-1]).style.opacity = '0.5';
    //console.log('coucou');
    w_tha.style.fontFamily = "NotoThai";
    pur.style.fontFamily = "NotoThai";

  } else {
    document.getElementById(langues[(i-1)]).style.opacity = '0.5';
  }
  i++;
}

Possibles améliorations ?

Une possibilité serait de faire une édition en conservant le même principe, mais en compilant toutes les combinaisons possibles de langues.

PORTRAIT-ROBOT

Lien GitLab du projet

GitLab

Partie concept

Intention

Mon objectif est d'utiliser mon Raspberry Pi comme d'une personne qui utiliserait divers réseaux sociaux en suivant uniquement ce que les algorithmes de recommandation lui conseillent. Je veux créer principalement un compte Google qui me permettrait de créer les autres comptes en les liant à celui-là. Mon programme tournerait en continu sur le Raspberry et regarderait en boucle des vidéos sur Youtube, écouterait une playlist sur-mesure sur Spotify, suivrait les comptes recommandés par Twitter, etc. Je veux voir ce que les différents algorithmes vont créer comme personnalité à ce faux compte. C'est une boucle de rétroaction crée par l'usage : l'utilisation même détermine cette utilisation.

Scénarios

Pour l'instant le scénario de mon "utilisateur qui ne choisit pas" (appelons-le Philippe) est de se connecter sur un réseau social, cliquer sur le premier lien disponible et de suivre sans interruption ce que lui propose les algorithmes de recommandation pendant une durée indéfinie. C'est un comportement protocolaire qui n'est pas très réaliste, mais il évoque bien la "zone de la machine".

Sur Youtube, il clique sur le premier lien de la première ligne et continue à regarder ce qui se trouve dans la case "up-next". S'il s'arrête (quand je le débranche), la fois d'après il reprend à partir de la dernière vidéo dans le log (un fichier texte qui s'agrandit de liens au fur et à mesure).

Sur Spotify même principe, sauf que le bot enregistre le titre/le nom de l'artiste pour chaque morceau.

Sur Twitter, il clique sur la première personne recommandée dans "Who follow ?" et prend une capture d'écran et enregistre le nom de la personne en question. Il s'abonne/aime le profil pour que l'algorithme de Twitter affine ses préférences. Philippe saute de personne en personne indéfiniment.

Sur Facebook même principe que Twitter.

L'objectif maintenant c'est de trouver une narration, de fictionnaliser le comportement de Philippe pour le rendre plus plausible en tant que personne, pour être plus raccord avec mon propos.

Quel autre comportement Philippe peut-il avoir ?

  • Cliquer sur une vidéo/un article/un post au hasard parmi la première page ? Parmi la première ligne ?
  • Cliquer sur la vidéo la plus "visible", attirante à l'oeil (avec un titre uniquement en CAPITALE ? Le thumbnail avec le plus de contraste ?)
  • Regarder des vidéos/articles/posts en boucle à l'infini ?
  • Seulement pendant des horaires de bureau, comme si c'était un bullshit job ? Est-ce que son "emploi du temps" est séparé avec 2h de Facebook/2h de YouTube/2h d'Instagram/2h de Twitter ?
  • Regarder du contenu seulement pendant la nuit, pendant l'inverse des horaires de bureau -> comme pour une passion, un seul divertissement en dehors du cadre du travail ?
  • Le faire se comporter comme un alter ego qui est actif quand je ne suis pas actif ? (Quand je dors globalement)
  • Est-ce que Philippe reste une longue période sur la même plateforme avant de changer ? Quelle durée ? 1 semaine ? 1 mois ?
  • Est-ce que Philippe se déplace (migrations pendulaires ?) ? Est-ce qu'il peut voyager entre les pays en prenant le bus VPN ou rentrer en France pendant les vacances ?
  • Est-ce que Philippe change d'ordi de temps en temps et s'active sur le mien ou celui de mes colocs ?
  • Est-ce que Philippe passe les pubs ou bien est-ce que'il les regarde toutes assidûment (voire même clique dessus) ?

Réfléchir à la relation entre "l'agentivité" de l'usager, sa capacité à prendre des décisions et à agir, et la construction d'une figure abstraite complètement déterminée par l'algorithme ?

Apparemment malgré ces dispositifs de facilitation/suppression du choix et de l'autonomie, les utilisateurs réels sont la plupart du temps quand même en autonomie. Ils n'ont recourt que ponctuellement aux algorithmes de recommandation, parfois dans des contextes bien particuliers (quand ils écoutent de la musique en arrière-plan en faisant autre chose par exemple. (cf. article LES ALGORITHMES DE RECOMMANDATION MUSICALE ET L’AUTONOMIE DE L’AUDITEUR, Jean-Samuel Beuscart, Samuel Coavoux et Sisley Maillard)

L'article soulève aussi la question du goût : est-ce que ces algorithmes créent des utilisateurs dépassionnés, sans jugement de goût et sans réelles préférences ?

Questionner l'objectif dans l'utilisation de certains réseaux : Twitter plus politique que Youtube ou Spotify ?
Quel partie de la personnalité je veux construire à travers ce projet, dans quel domaine je veux que Philippe ait une opinion ?


Philippe ?

Est-ce que Philippe est le bon nom ? Est-ce que genrer Philippe est une bonne idée, ou bien un prénom mixte (comme Dominique) correspondrait mieux vu que l'on parle d'un bot/programme ? Liste de prénoms mixtes (qui s'écrivent pareil) d'après Wikipédia (j'ai enlevé les moins ambigus) :

  • Dominique
  • Camille
  • Alex
  • Alix
  • Sacha
  • Ange
  • Claude
  • Lou
  • Candide
  • Charlie (apparemment le mieux réparti au niveau de la parité [4])
  • Yaël (bien réparti aussi)
  • Céleste
  • Andrea
  • Louison (lui aussi)
  • Mahé
  • Gwenn
  • Loan
  • Philippe (apparemment très rarement féminin)

En clair, le problème est comment ne pas rattacher le bot à une norme de représentation.

Cependant le programme va tourner sur un Raspberry en Belgique, il sera situé et sa navigation en sera impactée, donc je pourrais peut-être choisir un prénom mixte en fonction des statistiques de prénoms belges ? L'idée est de partir d'une base d'utilisateur.ice lambda, dans la moyenne, pour voir ce que les algorithmes font de cette base.

Est-ce que le bot a même besoin d'un prénom ? La raison pour laquelle j'y tiens c'est pour l'adresse mail et tout ce qui tourne autour, et que pour moi la personnalité/l'identité s'ancrent dans le prénom. Pourquoi donner des noms aux choses est compliqué ?

En cherchant des statistiques ethnographiques sur les catégories de personnes qui regardent YouTube, je suis tombé sur cet article de Google, qui vante les mérites de YouTube. Il nous apprend seulement que plus de la moitié des gens entre 16 et 44 ans en France vont tous les jours sur YouTube (2016). Mais il nous renseigne sur la façon dont Google veut que YouTube soit perçu : un lieu d'ouverture, de partage, de multiplicité des choix, de liberté, et donc pas du tout comme je veux le montrer : une machine qui restreint les choix et peut enfermer dans une bulle de filtre.

Dans les prénoms mixtes, j'aime bien Dominique pour faire un jeu de mot avec le DOM (pas super pertinent mais renvoie au web), mais le prénom est un peu vieillot (et donc ne rentre pas trop dans la catégorie 18-35 ans qui sont les plus actifs sur YouTube).

J'aime aussi Gwenn, qui sonne bien comme diminutif de pleins de prénoms possibles. Aussi, si je considère le bot comme mon espèce d'alter ego (puisqu'il me suit physiquement et est localisé dans mon appartement), le fait que ce soit un prénom d'origine bretonne colle bien. Définition d'épicène.

Mais ces prénoms sont assez localisés géographiquement/culturellement, et j'ai un peu un dilemme entre un prénom choisi pour sa neutralité ou avec des statistiques (mais rien n'est vraiment neutre), ou alors un prénom qui donne déjà une connotation/un ancrage culturel.

Pour l'adresse mail et pour créer un compte (nom d'utilisateur et tout), il faudrait peut-être aussi un numéro, style <gwenn58@gmail.com> ?
, et un nom de famille. Quel numéro peut avoir du sens ?
Souvent c'est une date de naissance ou un numéro lié à l'endroit où l'on habite.

Décisions

Horaires

Je veux que le bot soit comme une personne qui travaille avec des horaires de bureaux toutes la semaine, et utilise les réseaux sociaux comme moyen principal de divertissement/comme hobby. Ses horaires de bureau seraient 8h30-12h et 13h30-17h30, du lundi au vendredi. Le bot serait actif sur la pause de midi, le soir et le weekend, soit :

  • de 12h30-13h30, 18h30-19h30 et 21h-23h du lundi au vendredi
  • 10h-12h, 13h-19h30, 21h-00h le weekend

(en comptant des moments de repas + sommeil). Il faut que je me renseigne sur cron pour la planification des horaires de fonctionnement du programme.

Matérialité

Le programme tournerait sur mon Raspberry Pi, le bot aurait donc une matérialité physique. Il se déplace avec moi si je change d'appartement,

Localisation

Le Raspberry fonctionnerait dans mon appartement, à Bruxelles. Il dépend de ma localisation.

Réseaux

Dans un premier temps le bot utiliserait uniquement YouTube (car je ne suis pas sûr d'avoir le temps de creuser le projet avec Spotify ou Twitter par exemple), mais mon envie de départ était que le bot soit multi-plateforme.

Pubs

Pour l'instant le bot utilise une extension qui passe les pubs (je ne suis pas sûr de la fiabilité de cet article mais cela semble assez répandu pour que je le considère sur une personne "lambda".

Le doute ?

Je me prend un peu la tête sur la définition de la personnalité du bot/sa personnalité pour le rendre humain, mais ce qui m'intéresse c'est plus les résultats que l'expérience va produire. Je vais plutôt me concentrer sur des protocoles multiples, pour voir les différents types de résultats ça peut produire. Plutôt que de me concentrer sur un choix ultime, je m'autorise à changer de compte par exemple, en changeant les paramètres du bot. Le numérique me permet une démultiplication des identités du bot, qu'il ait des alias, des forks, et que finalement c'est plus intéressant que d'imiter de manière automatique un comportement humain. Le nom du bot peut être multiple, et je cherche un nom de figure/personnage fictif, dont l'histoire incarnerait les thèmes de mon projet.

J'ai pensé ironiquement à un nom de personnage qui fait face à un dilemme cornélien, alors que le bot fait le moins de choix possible.

Pour ce premier bot j'ai choisi Cinna, et puisque la pièce de Corneille a été écrite en 1643 son compte Google sera [cinna1643@gmail.com]. Ce bot tourne 24/24h, regarde les vidéos en entier et passe les pubs.


Mise en forme

Vidéo

Avec les 22 premières vidéos que j'ai récupérées, je voulais faire une sorte de méga mashup en fusionnant 1 seconde de chaque vidéo, pour faire une sorte de parcours condensé du parcours du bot. Pour cela j'ai utilisé ffmpeg, qui permet de faire des manipulations vidéo sur plein de fichiers en même temps (comme ImageMagick pour les images par exemple) en ligne de commande. J'ai essayé de faire un script bash qui permet de couper chaque vidéo pour garder 1 seconde, et ensuite de fusionner toutes ces vidéos entre elles. Je pense que j'ai des problèmes de codecs puisque l'audio est décalé et qu'à un moment l'écran devient complètement vert. Toutes les vidéos YouTube ne sont pas dans le même format étonnamment, je pense que c'est une question de réglages. Avec ffmpeg on peut aussi faire des filtres pour convertir d'un coup plusieurs vidéos dans le même codec mais ça m'a l'air compliqué, je vais creuser.

J'ai réussi à faire une version test avec Premiere Pro, mais dans l'idée il faudrait que je puisse le faire automatiquement avec ffmpeg sur le RPi. Peut-être que le traitement (téléchargement/coupe/fusion des contenu) peut se faire pendant que Philippe est "off" (qu'il ne parcourt pas de contenu) ?

Le résultat n'est pas très satisfaisant. Trop rapide ou trop direct.

Mise en espace/installation

Pour l'instant quand je lance Selenium il y a une interface graphique qui se lance, donc je peux voir sans toucher à rien le bot qui s'active et "fait sa vie". Cette étape est assez fascinante et flippante, c'est un peu comme si Philippe prenait le contrôle et lançait des vidéos comme ça sur mon ordi pendant que je travaille dessus. Alors sur Youtube une fois le premier clic sur une vidéo passé c'est pas très intéressant, mais sur un autre réseau avec plus d'interactivité (scroll par exemple ?) cette vidéo peut-être parlante, et agir presque comme un test de Turing si on la voit sans le contexte.

Problème : comment faire pour que ça n'ait pas l'air d'une simple vidéo en accéléré de quelqu'un sur un ordi ? Une possibilité serait de le montrer en live, et donc de donner un côté performatif à l'expérience.

  • Peut-être que le programme peut être lancé sur un ordi (avec crontab ?) qui l'active/l'arrête à des heures fixes (et donc on verrait les prints dans un terminal de cron et du programme, quand selenium n'est pas actif ?
  • Il faudrait pouvoir visualiser d'une façon ou d'une autre le clic ou le mouvement de souris, pour retrouver ce côté automatique qu'il y a dans le remplissage du formulaire.
  • Peut-être qu'il peut-être streamé en direct depuis cet ordi ? Philippe aurait une chaîne Twitch pour partager sa passion de la recommandation ?

Voir le déroulement en live accentue le côté voyeur, comme si on espionnait quelqu'un devant son ordi pendant qu'il se détend, drôle de sensation. L'objectif est de nous mettre dans une position de voyeur alors qu'on a affaire a un robot.

Matériellement, quel dispositif pour montrer cette vidéo ?

  • Il faudrait un environnement de bureau, un fauteuil avec du café/thé ?), personnalisé (peut-être par rapport aux passions que Philippe a acquis pendant l'expérience). Ambiance casual et intime (le voyeurisme tout ça).
  • Quel type d'ordi ? Philippe est un "monsieur tout le monde" Un article qui peut donner une piste ?
  • Dans une petite pièce, un vrai bureau ou dans une pièce qui n'a rien à voir ?


Peut-être que toutes mes expérimentations d'AP peuvent se retrouver sur ce bureau (éditions, sites/interfaces dans d'autres fenêtres…), et alors on fouillerait dans ses tiroirs et sur son bureau pour enquêter sur lui ? (lien avec le portrait-robot ?)

La question mon alter-ego revient, puisque les intérêts de Philippe seraient alors confondus avec les miens (mon sujet de la recherche de cette année). Il pourrait alors y avoir deux bureaux, un bureau qui est celui de Philippe étant le résultat et fonctionnant indépendamment, et mon bureau, avec les dessous, les projets parallèles et connexes qui viennent nourrir le projet et apporter de l'interactivité (expériencer des sites sans recommandation, aller sur le "blog" de Philippe ou voir les logs du RPi).

Interface

L'idée serait peut-être aussi de faire une interface, comme un journal de bord de ce parcours à travers les algorithmes (en modifiant l'interface de la Pibliothèque ?). Avec les logs récupérés (URLs…), je peux mettre en page dynamiquement comme un blog, un agrégat qui serait le reflet de la personnalité de Philippe. Comment montrer cette personnalité, ce "portrait-robot" ? Comment rendre des logs narratifs ?

Je pourrais éventuellement intégrer une partie d'étude statistique (à partir des tags par exemple) pour essayer de trouver des tendances dans le comportement de Philippe, un peu comme une étude sociologique de son profil. Ça peut renvoyer à la surveillance et au côté analytique des algorithmes, qui construisent ce genre de profil statistique pour construire des recommandations personnalisées, et en même temps être un moyen différent de montrer une personnalité.

Peut-être qu'une façon de rendre compte du parcours est d'enregistrer sous forme de phrases les différentes actions effectuées par le bot (Je clique à tel endroit, j'attends que la vidéo se termine, la page se recharge). Lecture par un text-to-speech pour évoquer l'aide pour les non-voyants (le narrateur vocal ?) ? Voix qui décrit à l'oral le comportement de l'utilisateur ?

J'ai essayé de donner une forme narrative à ce "log" (fichier texte qui regroupe différents messages concernant le fonctionnement d'un programme):

Je me connecte tout seul, c'est bien pratique
Je regarde la vidéo de la dernière fois.
Je clique sur le bouton "play" pour lancer la vidéo.
  Le titre de la vidéo est Et voilà les Shadoks, la saison 2 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=Dk1JjjbZ4yc]
      La vidéo dure 2:18:45.

Une nouvelle vidéo !
  Le titre de la vidéo est Et voilà les Shadoks, la saison 1 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=tpD0Pdr7oD0]
      La vidéo dure 1:33:51.

Une nouvelle vidéo !
  Le titre de la vidéo est Culte : c’était leur 1ère télé, allez-vous les reconnaître ? | Archive INA.
    L'url est [https://www.youtube.com/watch?v=XA3ScOmGr34]
      La vidéo dure 25:29.

Une nouvelle vidéo !
  Le titre de la vidéo est 5 inventions cultes des années 90 | Archive INA.
    L'url est [https://www.youtube.com/watch?v=ueaZgk_qduw]
      La vidéo dure 13:04.

Cette interface sera sûrement en trois parties : l'une aura la forme d'un blog, d'un moyen de visualiser temporellement la progression du bot, qui raconte ce qu'il est en train de faire et commente au fur et à mesure les particularités de son parcours. Le blog symbolise aussi une personne qui raconte son expérience personnelle, c'est donc adapté par rapport à ma thématique.
La deuxième partie sera plus visuelle, et sera une séquence des thumbnails des vidéos. C'est une autre manière de ressentir le parcours du bot, de lui donner un aspect narratif par l'image aussi.
La troisième pourra faire le lien entre les deux premières, et sera une couche plus analytique, qui fera ressortir peut-être des éléments statistiques/sociologiques sur la personnalité du bot (en fonction des tags des vidéos par exemple, en utilisant des outils tels qu'Iramuteq ?) ou les catégories YouTube les plus présentes (Divertissement, Éducation, Sport etc.), en tout cas apporter un recul sur le parcours dans sa globalité.

V1

Pour l'instant l'interface ressemble à ça :

PARTIE TEXTE
Interface v1 texte.png

PARTIE IMAGES
Interface v1 images.png

Pour passer de l'une à l'autre pour l'instant il faut cliquer sur le bouton "IMAGES". Interface v1 toggle.gif

La police utilisée est Happy Times at the IKOB New Game Plus Edition de Lucas Bihan. Je cherchais une police serif pour donner un côté plus "littéraire"/narratif, puisque le bot raconte son parcours. Cette police est une version contemporaine et libre de la Times New Roman.

J'ai rajouté la possibilité d'ouvrir la vidéo en question quand on clique sur le titre directement dans le post. Interface v1 clic.png

V2

Je voulais que sur l'interface on puisse à la fois réagir en live à l'avancement du bot, mais en même temps servir d'archive pour voir la progression dans la durée. Finalement, pour que tout ça soit plus cohérent, les deux parties ne doivent pas forcément cohabiter autant, au moins pas dans la forme qu'à la première version.

Une donnée importante du projet est le temps, puisque le bot fonctionne en temps réel, et regarde entièrement chaque vidéo (et chaque pub). Il faudrait donc que je trouve comment faire une partie "chargement", ou bien de trouver un moyen de marquer l'évènement "nouvelle vidéo" de façon à créer une attente si l'on reste sur la page. Le but étant de donner en vie d'attendre pour voir quelle sera la prochaine vidéo vue par le bot.

J'aimerais que l'interface ait un côté plus "littéraire/narratif". Pour cela je veux que la partie archive ait plus l'air d'un journal intime qu'un log ou qu'une page Twitter. C'est plus ce côté "carnet" qui m'intéresse qu'un côté très numérique. J'ai trouvé les sites de monkkee et penzu, qui sont des journaux intimes en ligne, pour essayer de voir la forme que ça prend. En fait ça ressemble juste à un éditeur de texte en ligne, mais on peut personnaliser l'aspect de la page.

Penzu exemple.jpg Monkey exemple.jpg

Pour passer de la partie live à la partie archive, j'ai pensé à un dispositif comme celui du site de Spector Books, avec un moyen de glisser avec la souris pour passer de l'un à l'autre comme si on faisait des aller-retours avec les pages d'un livre pour consulter des notes ou un index.

Pour la partie image j'ai pensé qu'au lieu de faire une séquence d'images à la verticale, où on devrait descendre en scrollant, je pourrais faire comme un flip-book. L'image suivante remplacerait la précédente lorsque l'on scrolle.

Maintenant j'utilise la police EB Garamond, une version numérisée et libre d'une Garamond, qui conserve des aspérités ou imperfections de l'impression au plomb.

En observant les gabarits de Facebook, Twitter ou YouTube j'ai remarqué qu'il y a souvent 2 ou 3 colonnes qui fonctionnent indépendamment les unes des autres (on peut scroller dans chacune d'elle séparément). La colonne du milieu est toujours là où se trouve le principal, les autres colonnes servant à afficher des éléments supplémentaires, des liens pour la navigation, des pubs ou des contenus recommandés. Je voudrais donc partir de cette configuration un peu comme base.

Partie technique

Collecte

Extension Javascript

J'ai d'abord essayé de faire une extension de navigateur pour Firefox, qui enregistre l'URL à chaque fois que la lecture automatique change de vidéo (j'ai commencé par YouTube) et qui m'envoie la liste par mail tous les 10 vidéos (les extensions n'ont pas d'accès à la possibilité d'écrire ou de lire sur un fichier local). Pour ça j'ai utilisé les Mutation Observer de javascript, qui permettent de lancer des instructions quand certaines mutations apparaissent dans le DOM, ainsi que la librairie smtp.js, qui grâce au serveur smtp de Gmail, me permet de m'envoyer des mails. Les extensions de navigateurs fonctionnent de la même façon sur Chrome ou Firefox :

  • le fichier manifest.json sert à régler différents paramètres. Il peut par exemple servir de filtre pour choisir les onglets ou les noms de domaines dans lesquels les scripts s'éxécutent
  • le "content-script" (ici espion.js) peut accéder au contenu du DOM de la page dans laquelle il est lancé (récupérer des infos et modifier la page en js). Il peut envoyer des informations au "background script" et vice-versa
  • le script background.js peut effectuer plus d'actions (avoir accès à des APIs ou ce genre de choses) mais ne peut pas accéder au DOM
manifest.json
{
"manifest_version": 2,
  "name": "Espion",
  "version": "1.0",

  "description": "Enregistre toutes les urls des vidéos lancées par l'onglet up next de Youtube et les musiques de Spotify",

  "background": {
    "scripts": ["background.js"]
  },

  "icons": {
    "48": "icons/border-48.png"
  },

  "content_scripts": [
    {
      "matches": ["*://*.youtube.com/*", "*://*.spotify.com/*"],
      "js": ["espion.js"]
    }
  ]
}
espion.js
///////////////////////////FONCTIONS/////////////////////////////////
function handleResponse(message) {
  console.log(`${message.response}`);
}

function handleError(error) {
  console.log(`Error: ${error}`);
}

function notifyBackgroundPage(e) {
  var sending = browser.runtime.sendMessage({
    test: e
  });
  sending.then(handleResponse, handleError);
}

/////////////////////SCRIPT///////////////////////////////////////////
console.log('coucou');

var liens = [];
var txt;
var count = 0;

var observer = new MutationObserver(function(mutations) {

  // For the sake of...observation...let's output the mutation to console to see how this all works
	liens.push(window.location.href);
  console.log('yes');

  if (count == 4) {
    notifyBackgroundPage(liens);
    liens = [];
    console.log(liens);
  }

  count = (count+1)%5;
  console.log(count);

});

// Notify me of everything!
var observerConfig = {
	childList: true,
	characterData: true,
};

// Node, config
var targetNode = document.getElementById('movie_player');

observer.observe(targetNode, observerConfig);
background.js
/* SmtpJS.com - v3.0.0 */
var Email = { send: function (a) { return new Promise(function (n, e) { a.nocache = Math.floor(1e6 * Math.random() + 1), a.Action = "Send"; var t = JSON.stringify(a); Email.ajaxPost("https://smtpjs.com/v3/smtpjs.aspx?", t, function (e) { n(e) }) }) }, ajaxPost: function (e, n, t) { var a = Email.createCORSRequest("POST", e); a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), a.onload = function () { var e = a.responseText; null != t && t(e) }, a.send(n) }, ajax: function (e, n) { var t = Email.createCORSRequest("GET", e); t.onload = function () { var e = t.responseText; null != n && n(e) }, t.send() }, createCORSRequest: function (e, n) { var t = new XMLHttpRequest; return "withCredentials" in t ? t.open(e, n, !0) : "undefined" != typeof XDomainRequest ? (t = new XDomainRequest).open(e, n) : t = null, t } };

var i = 0;

function sendEmail(txt) {
	i += 1;
	Email.send({
	Host: "smtp.gmail.com",
	Username : "***************************",
	Password : "**********",
	To : "adresse@mail.destinataire",
	From : "adresse_expéditeur@gmail.com",
	Subject : "youtube" + i,
	Body : txt,
	}).then(
		message = console.log("Mail bien envoyé !")
	);
}

console.log('salut');

function handleMessage(request, sender, sendResponse) {
  console.log(request.test);
  sendResponse({response: "Bien reçu !"});
	sendEmail(request.test);
}

browser.runtime.onMessage.addListener(handleMessage);

Le problème est que la version de Firefox sur Raspbian n'est pas optimisée pour le Raspberry Pi, et Firefox plante assez rapidement. En essayant avec Chromium (qui est apparemment optimisé au maximum), ça marche beaucoup mieux mais ça plante aussi quand le mail s'envoie.

J'ai quand même réussi a choper le lien de 22 vidéos à la suite avant que ça plante, en bidouillant un peu. Avec le paquet youtube-dl [5] (sur Linux, je sais pas s'il existe ailleurs) on peut en une commande télécharger toutes les vidéos si elles sont dans un fichier texte correctement formaté.

Selenium (Python)

Je vais essayer avec Selenium, qui est un navigateur utilisable par ligne de commande, qui peut aussi s'utiliser 'headless', c'est-à-dire sans interface graphique. On peut l'utiliser avec Python et donc le lancer automatiquement dès que je branche le Raspberry Pi.

Pour l'instant j'arrive à me connecter à YouTube, à lancer une vidéo, à passer les pubs et détecter quand la vidéo change. Pour ça j'ai dû utiliser une astuce pour ne pas que Chromium me détecte comme un robot (il ne me laissait pas me connecter à YouTube car il trouvait ça dangereux). Il faut lancer une commande dans selenium qui change la propriété webdriver du navigateur en "undefined". Ensuite il faut bidouiller avec le user-agent pour pouvoir avoir le Youtube qui tourne sur un navigateur récent.

Cette étape est sûrement différente sur le RPi parce que la technique utilisée fonctionne pour Chrome/Chromium au-dessus de la version 79, et la version de Raspbian doit être 76 ou 77 je crois.

Le début de mon script ressemble à ça (je ne l'ai pas encore essayé sur le RPi) :

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from time import sleep
from datetime import datetime

USERNAME = 'adresse.mail@gmail.com'
PASSWORD = 'mot_de_passe'

options = webdriver.ChromeOptions()
# options.add_argument('--profile-directory=Default')
# options.add_argument("--disable-plugins-discovery");
options.add_argument('--disable-extensions')
options.add_argument("--disable-blink-features=AutomationControlled");
options.add_argument("--start-maximized")
options.add_argument("--mute-audio")

options.add_experimental_option("excludeSwitches", ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
browser = webdriver.Chrome(options=options)
browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  "source": """
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined
    })
  """
})
browser.execute_cdp_cmd("Network.enable", {})
browser.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"}})

wait = WebDriverWait(browser, 4)

###vieux user-agent
# browser.execute_cdp_cmd("Network.setExtraHTTPHeaders", {"headers": {"User-Agent": "browserClientA"}})

#-------CONNECTION À YOUTUBE---------------

# browser.get('https://accounts.google.com/signin/v2/identifier?service=youtube&uilel=3&passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den%26next%3Dhttps%253A%252F%252Fwww.youtube.com%252F&hl=en&ec=65620&flowName=GlifWebSignIn&flowEntry=ServiceLogin')
#
# emailElem = browser.find_element_by_id('identifierId')
# ###vieux user-agent
# # emailElem = browser.find_element_by_id('Email')
# emailElem.send_keys(USERNAME)
#
# elem=browser.find_element_by_xpath('//*[@id="identifierNext"]/span/span')
# ###vieux user-agent
# # elem=browser.find_element_by_id('next')

# elem.click()
#
# browser.implicitly_wait(4)
#
# passElem=browser.find_element_by_name('password')
# ###vieux user-agent
# # passElem=browser.find_element_by_id('Passwd')
# passElem.send_keys(PASSWORD)
# elem=browser.find_element_by_xpath('//*[@id="passwordNext"]/span/span')
# ###vieux user-agent
# # elem=browser.find_element_by_id('signIn')
# elem.click()

# sleep(4)

#-----------------LANCER LA DERNIÈRE VIDÉO ENREGISTRÉE DANS LE LOG----------------------
###choper la dernière url enregistrée dans le log pour recommencer depuis l'arrêt du programme
# browser.get('https://www.youtube.com/watch?v=HWxXeoHndgY')
browser.get('https://www.youtube.com/watch?v=Wlyq22ybsRw')

###cliquer sur le bouton pour lancer la vidéo
elem=browser.find_element_by_css_selector('#movie_player > div.ytp-cued-thumbnail-overlay > button')
# elem=browser.find_element_by_xpath('//*[@id="movie_player"]/div[5]/button')
elem.click()


#----------------PASSER LA PUB----------------------
def passe_pub(temps):
    try:
        elem= WebDriverWait(browser, temps).until(EC.presence_of_element_located((By.CSS_SELECTOR, "#skip-button\:17 > span > button > span")))
        elem.click()
    except:
        print('pas de pub :)')

    titre = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#container > h1 > yt-formatted-string")))
    print(titre.get_attribute("textContent"))

#----------------RÉCUPÉRER LA DURÉE DE LA VIDÉO---------------
def duree_vid():
    durée = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "span.ytp-time-duration")))
    dur = durée.get_attribute("textContent")

    if len(dur) <= 5:
        pt = datetime.strptime(dur,'%M:%S')
        total_seconds = pt.second + pt.minute*60
    else:
        pt = datetime.strptime(dur,'%H:%M:%S')
        total_seconds = pt.second + pt.minute*60 + pt.hour*3600

    ###commande pour choper et afficher l'url actuelle
    print(browser.current_url)
    return total_seconds

#---------------REPÉRER LE CHANGEMENT DE PAGE (CLIQUER SUR LE BOUTON UP-NEXT)------------------------

# def chgmt_page(time):
    # up_next = '#movie_player > div.ytp-upnext.ytp-player-content.ytp-suggestion-set > a'
    # WebDriverWait(browser, timeout=time+1).until(EC.element_to_be_clickable((By.CSS_SELECTOR, up_next)))
    # elem=browser.find_element_by_css_selector(up_next)
    # print(elem.text)
    # elem.click()


def vid():
    # passe_pub(6)
    time = duree_vid()
    print(time)
    # passe_pub(time)
    # chgmt_page(time)
    upnext = browser.find_element_by_css_selector('#dismissable > div > div.metadata.style-scope.ytd-compact-video-renderer > a')
    upnext = upnext.get_attribute("href")
    WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
    print('nouvelle vidéo !')
   
try:
    while True:
        vid()
except KeyboardInterrupt:
    raise
except:
    print('Le programme a planté :/')

Peut être que pour des plateformes comme Facebook ou Twitter je peux utiliser un système de bot (pouvant être hébergé sur le RPi quand il est en fonctionnement) plutôt qu'un protocole complexe sur Selenium ? Naviguer entre les utilisateurs en les taggant ou en retweetant leurs posts ? -> La page Twitter en soi est un témoin de son passage et de la construction d'une personnalité, d'un avis.

Pour l'instant les pubs YouTube font tout planter, j'essaie de voir comment importer une extension dans Selenium (au moins temporairement) pour pouvoir mettre au point le truc sans les pubs. J'ai réussi à empaqueter des extensions au format .crx pour pouvoir lancer Selenium avec cette extension (AdBlocker), mais ça ne fonctionne pas et Selenium plante tout simplement.

J'essaie de régler le problème de mon programme qui n'arrive pas à cliquer sur "Skip Ad" (peut-être un problème de sélecteur CSS/Xpath). Ça à l'air de marcher avec ce sélecteur (pour le bouton "Skip Ad") :

"#skip-button\:8 > span > button"

(Même si je n'ai pas trouvé à quoi correspond le :8)

Un autre problème apparaît maintenant (23/04/2020) : Google a mis à jour la détection de robots/automates, et donc le moyen que j'avais trouvé pour contourner la sécurité de Youtube ne marche plus. Quand je veux me connecter, Youtube repère que j'utilise Selenium et considère que ce n'est pas sécurisé.

Ça marche plus.png

Sur l'image on peut voir à droite de la barre d'URL que les cookies ne sont pas activés, peut-être que le problème est que Selenium par défaut crée des sessions qui ne ne laissent pas de cookies ?

J'ai peut-être une piste qui utiliserait les cookies de connexion, pour me connecter automatiquement sans passer par la page de Google qui détecte Selenium.

Autre solution ?

Apparemment c'est possible de créer un profil utilisateur sur Selenium qui est gardé en mémoire dans un dossier, et qui comprend les cookies (et donc les infos de connexion) ainsi que les extensions. Si j'arrive à faire fonctionner ça j'élimine le problème de la connexion et des pubs en même temps.[6] Mais le souci reste le même : il faut que je puisse me connecter une première fois dans la page ouverte par Selenium pour enregistrer toutes les infos. Le navigateur ne détecte plus que je suis un robot, mais il ne me laisse pas me connecter.

Cookies ?

Selenium permet de recupérer les cookies d'une page avec browser.get_cookies(), mais le problème c'est qu'il faut déjà être connecté pour récupérer les cookies de connexion.[7]Donc il faut que je me connecte avec Selenium pour récupérer les cookies qui me permettront de me connecter sans la page de connexion avec Selenium. Puisque je ne peux pas me connecter même manuellement dans la page ouverte par Selenium, je ne peux pas récupérer les cookies de cette façon.

Pour contourner ça j'ai essayé de récupérer les cookies depuis une session manuelle de Chromium. [8] J'arrive à avoir un fichier de cookies au format netscape, mais Selenium n'accepte que les fichiers de cookies sous forme d'objets JSON "sérialisés", donc je cherche maintenant un moyen de conversion. J'ai trouvé ce site (en russe) qui permet cette conversion. Un objet sérialisé est une variable convertie sous forme de fichier ou de chaîne de caractères qui peut donc être partagée et réutilisée dans un autre script. La sérialisation c'est le processus de conversion d'un objet dans un format binaire transportable et reconstructible pour créer un clone de cet objet dans un autre programme. La désérialisation c'est l'étape de reconstruction de l'objet dans le programme. En Python pour faire ça on utilise le module Pickle. Apparemment il n'est pas sécurisé, mais vu que je ne vais l'utiliser en local c'est pas très grave.

J'ai réussi la conversion, je copie-colle dans pickling.py les cookies au format JSON, je change quelques trucs (transformer les objets JSON 'true', 'false' et 'null' respectivement en 'True', 'False' et 'None', qui sont les équivalents en Python) et je "pickle" le résultat dans cookies.pkl :

pickle.dump(to_pickle, open("cookies.pkl","wb"))

Le mode "wb" est pour "write binary", puisque pickle convertit l'objet en binaire pour l'enregistrer.

On obtient bien un truc illisible:

��]q

(je ne peux pas tout copier apparemment).

Et pour "unpickle" dans mon programme principal :

cookies = pickle.load(open("cookies.pkl", "rb"))

Quand j'importe les cookies dans Selenium ça plante (apparemment un des cookies a un mauvais nom), j'essaie de voir lequel pour corriger ça.

Petit debugging :

for i, cookie in enumerate(cookies):
    try:
        browser.add_cookie(cookie)
    except:
        print('Le cookie %s ne marche pas' % i)

C'est le dernier cookie qui ne marche pas, j'ai sûrement copié-collé une ligne vide en trop. J'ai aussi enlevé la première qui était juste la ligne disant que le cookie était au format Netscape.

Les cookies sont maintenant importés. Il faut sûrement recharger la page pour qu'ils prennent effet:

browser.refresh()

Et ça marche :).

J'arrive à me connecter sur YouTube à nouveau, même si c'est moins impressionnant que de voir les formulaires qui se remplissent tous seuls.

Je vais alors essayer de créer un profil utilisateur pour inclure les extensions qui bloquent les pubs.

Passer les pubs

Je crois que j'avais simplement oublié de désactiver l'option --disable extensions, ce qui faisait planter Selenium quand je chargeais une extension. J'arrive maintenant à charger des extensions sans soucis (oups).

Petites corrections

Pour corriger le problème du dédoublage des liens dans le log (quand une nouvelle vidéo se lance, le programme détecte 6-7 changements d'URL et relance la boucle 6-7 fois, ce qui est absurde. Il suffit de rajouter sleep(1) à la fin de la boucle pour prendre en compte le chargement de la nouvelle page.

def new_vid():
    """Fonction qui lance un protocole à chaque changement de page."""
    get_infos()
    time, tps = duree_vid()
    print('      La vidéo dure %s.' % tps)  #NARRATIF
    upnext = browser.find_element_by_css_selector('#dismissable > div > div.metadata.style-scope.ytd-compact-video-renderer > a')
    upnext = upnext.get_attribute("href")
    print('Je regarde la vidéo jusqu\'au bout.')
    WebDriverWait(browser, time+10).until(EC.url_to_be(upnext))
    print('\nUne nouvelle vidéo !')  #NARRATIF
    sleep(1)

Ensuite il faut corriger le problème de la duplication du dernier lien (quand on ouvre la dernière vidéo on ré-enregistre son URL). Au moment de l'enregistrement on vérifie que l'URL actuelle n'est pas la même que la dernière de la liste.

with open('log_urls.txt', 'a+', encoding='utf8') as f_2:
    last_vids = f_2.read()
    last_vids = last_vids.splitlines()
    if last_vids[len(last_vids)-1] != browser.current_url:
        f_2.write(browser.current_url + '\n')

Headless ?

Maintenant je veux essayer de faire tourner le programme en mode "headless" (sans interface graphique), pour que ce soit plus rapide et moins gourmand, pour pas que ça ne fasse planter le Raspberry. Il faut juste rajouter

options.add_argument("headless")

dans les options de Selenium.

Chrome et Firefox ont tous les deux des modes headless, mais pour Chrome dans ce cas-là on ne peut pas utiliser d'extensions, et il faudrait que je passe les pubs manuellement en cliquant sur "Skip Ad".
On peut apparemment utiliser des extensions dans Firefox, mais Firefox est mal optimisé pour le Raspberry. Je pourrais essayer mais il faut que je réfléchisse à la connotation donnée par ces choix en fonction de la personnalité de "Philippe".

  • Avec Firefox ce serait plus facile à mettre en place, mais le choix de Firefox en tant qu'utilisateur sous-entend peut-être une personnalité qui se soucie de sa vie privée sur Internet.
  • Chromium demanderait que je fasse de la bidouille pour passer les pubs manuellement (ce qui serait plus en accord avec l'idée d'un. utilisateur.ice lambda, qui n'a pas forcément d'adblocker) mais serait plus logique (Chrome est le navigateur le plus utilisé, + de 64% des gens l'utilisent en Novembre 2019 d'après Wikipédia).

Ce n'est pas vraiment nécessaire que mon bot tourne en headless, puisque pour l'instant il va tourner chez moi.

Choix de la vidéo

Je vais le baser le choix de la première vidéo lancée par le bot sur le nombre de caractères en capitales dans le titre.

###fonction pour calculer la proportion de capitale dans une string
def nbr_uppr(string):
    uppercase = 0
    for c in string:
        if c.isupper():
            uppercase += 1
        else:
            pass
    proportion = uppercase/len(string)
    return proportion

###récupère tous les titres de vidéos de la page d'accueil
browser.get('https://www.youtube.com/')
titres = browser.find_elements_by_id('video-title')
        proportion_upper = []
        ###sélectionne seulement les 8 premiers titres (ceux que l'on peut voir quand on ouvre la page d'accueil)
        for r in range(0,8):
            str_titre = str(titres[r].get_attribute('textContent'))
            proportion_upper.append(nbr_uppr(str_titre))
        ###max(array) retourne la valeur la plus haute de l'array
        max_index = proportion_upper.index(max(proportion_upper))
        titres[max_index].click()

Sur le Raspberry

Maintenant que le programme fonctionne sur mon ordi, il faut le transférer sur mon ordi. J'ai essayé de refaire la même démarche que j'avais effectuée sur mon ordi, mais les chromedrivers disponibles au téléchargement sont adaptés à une architecture x64, alors que le RPi a une architecture x32. Je suis tombé sur ce thread reddit qui explique pas à pas comment faire pour avoir un chromedriver adapté au RPi (x32 et armhf). Le driver que j'ai trouvé est la version 65 et quelques, alors que Chromium est en version 78 sur le Rpi. Ça n'a pas l'air de poser de soucis pour l'instant.

Il faut maintenant que j'adapte un peu le code pour qu'il tourne sur le RPi (beaucoup plus lent, il faut rajouter des conditions pour être sûr que les éléments soient chargés avant d'essayer d'y accéder).

Le programme tourne sur le RPi, vraiment très lentement mais ça fonctionne. Malheureusement il plante au bout d'un moment. D'après mes tests parfois c'est Chromium qui crashe, parfois c'est c'est juste que la page ne charge pas bien. C'est à moitié un problème de connexion et à moitié un problème de mémoire peut-être. Je vais peut-être essayer de le faire tourner en headless.

Pour faire tourner le programme en headless il faut enlever l'extension qui permet de bloquer les pubs. Avant ça me posait problème parce qu'il fallait absolument que je passe les pubs pour récupérer la durée de la vidéo (et pas la durée de la pub), comme ça je pouvait indiquer à Selenium le temps d'attente avant qu'une nouvelle vidéo apparaisse. Mais en fait je peux mettre un temps très long (par exemple 5h) pour être sûr que le programme attende bien sagement la fin de la vidéo pour détecter le changement de vidéo, qui s'opère tout seul.

Sur mon ordi mon programme marche en headless, je n'ai plus qu'à le mettre sur mon RPi et le brancher en ethernet et c'est parti ! Pour le faire fonctionner sur le RPi, il faut modifier un peu le script. À un moment j'utilise un bloc except sans préciser d'erreur, ce qui fait que le programme ne plante pas s'il y a un problème à ce moment-là. Et puisqu'il tourne en headless, je ne peux pas voir s'il a planté.

Je modifie donc

###cliquer sur le bouton play pour lancer la première vidéo
try:
  elem = browser.fin_elements_by_css_selector("#movie_player > div.ytp-cued-thumbnail-overlay > button")
  elem.click()
except:
  continue

par

###cliquer sur le bouton play pour lancer la première vidéo
elem = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#movie_player > div.ytp-cued-thumbnail-overlay > button")))
elem.click()

qui attend que le bouton "play" soit chargé pour cliquer dessus et lancer la vidéo. https://serverfault.com/questions/96499/how-to-automatically-start-supervisord-on-linux-ubuntu Il me reste à trouver un moyen de "surveiller" l'activité du programme, pour le relancer automatiquement s'il plante.
Pour cela je vais utiliser supervisor, qui me permet de transformer un programme en "daemon", c'est-à-dire en tâche fonctionnant en arrière-plan. Je peux le paramétrer pour définir le nombre de fois que supervisor doit essayer de relancer le programme s'il plante, le chemin pour des fichiers de log etc.

Puisque le programme sera lancé par un autre programme (et donc pas depuis le dossier de travail), il faut que dans le script Python tous les chemins soient noté en absolu, et pas relativement au dossier dans lequel se trouve le programme (sinon ça va planter lorsque le script cherchera les cookies ou les logs par exemple).

Dans le fichier de configuration de supervisor (/etc/supervisord.conf) je rajoute

[program:portrait-robot]
command=python3 ~/Documents/BOULOT/AP/portrait-robot/portrait_robot.py
priority=0
startsecs=50
startretries=5
stdout_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_log.txt
stderr_logfile=~/Documents/BOULOT/AP/portrait-robot/logs/stdout_err.txt

qui correspondent aux paramètres énoncés plus haut.

Maintenant, il faut que je fasse en sorte que supervisor (et donc mon programme en python) se lance tout seul à chaque démarrage du RPi. Je suis les instructions de cette page.


Après un essai concluant je me rends compte que les vidéos trouvées par le Raspberry sont toutes en anglais, qui doit être par défaut le paramètre du compte Google. Il faut que je change ça et que je recommence pour avoir des vidéos en français.
J'ai changé les paramètres du compte Google et refait les manipulations pour obtenir les cookies, et cette fois-ci effectivement j'ai des résultats en français sur la page d'accueil.

Interface

Structure

Essai avec javascript

Je suis en train de construire une interface en PHP/javascript pour extraire les urls du log généré par le programme, et en extraire des informations (titre, durée, chaîne, thumbnails etc.) grâce à l'API de YouTube pour pouvoir les mettre en forme dynamiquement. ces informations me serviront à faire une "analyse" du contenu au moment où je le collecte. Je pense qu'une partie prendra une forme textuelle, comme une narration par le bot des actions effectuées (le log du programme python fait ça en partie, mais de manière très scriptée).
Le but est que le bot commente par des phrases du style "Hmmm j'en ai marre celle-là ça fait X fois que je tombe dessus", ou alors "J'aime bien les longues vidéos" si il y a 3 fois d'affilée des vidéos de plus de 2h, ce genre de choses.

Je récupère le contenu du fichier texte et l'affiche ligne par ligne dans des balises p :

  $fh = fopen('log_urls.txt','r');
    while ($line = fgets($fh)) {
      echo('<p class="url">'.$line.'</br></p>');
    }
    fclose($fh);

Je rends ces balises invisibles :

.url{
  display:none;
}

Je récupère le contenu (présent bien qu'invisible) dans une array :

var urls_collection = document.getElementsByClassName('url');
urls_array = Array.from(urls_collection);
urls = [];
//faire des éléments HTML récupérés une array d'urls
for (url of urls_array){
  //il faut enlever le caractère "\n" à la fin de chaque ligne
  strippedUrl = url.textContent.trim()
  urls.push(strippedUrl);
}

Je fais appel à l'API de YouTube avec une librairie javascript :

function loadClient() {
  gapi.client.setApiKey(apiKey);
  return gapi.client.load("https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest")
    .then(function() {
        console.log("GAPI client loaded for API");
      },
      function(err) {
        console.error("Error loading GAPI client for API", err);
      });
}
// Make sure the client is loaded and sign-in is complete before calling this method
function execute(url) {
  vid_id = url.replace('https://www.youtube.com/watch?v=', '');
  return gapi.client.youtube.videos.list({
      "part": "snippet",
      "id": vid_id
    })
    .then(function(response) {
        // Handle the results here (response.result has the parsed body).
        result = response.result.items['0'].snippet.title;
        // console.log('TITRE : '+result);
        // console.log(typeof ''+result);
        console.log(result)
        return {
          result:result
        };
      },
      function(err) {
        console.error("Execute error", err);
      });
}

Il faut aussi charger auparavant la librairie dans la page HTML/PHP.

 <script src="https://apis.google.com/js/api.js" type="text/javascript"></script>

Finalement, cette technique est compliquée puisqu'elle implique des histoires de code asynchrone, et de trouver un moyen pour être sûr que les actions de javascript (appels à l'API) se fassent dans le bon ordre, sinon ça ne fonctionne pas. En plus, cela voudrait dire qu'à chaque chargement de la page il y a autant de requêtes à l'API qu'il y a de vidéos collectées par le bot, ce qui va vite devenir ingérable (et très mal optimisé). Pour finir, si une vidéo est supprimée au moment de la consultation de la page, l'API ne pourra pas la trouver.

Appeler l'API avec Python

Le moyen le plus fiable/efficace est donc de récupérer les infos sur la vidéo (titre, durée, date de publication, description, commentaires, etc.) toujours avec l'API mais via Python/Selenium. De cette façon, on ne fait une requête à l'API qu'une fois par jour, l'interface Python est plus compréhensible pour moi, et je peux enregistrer le résultat au format JSON (qui sera très pratique à traiter en javascript ou en PHP).
J'obtiendrais donc un log en JSON sur le Raspberry, qui se remplira petit à petit. Ensuite il suffit que le Raspberry envoie sur le serveur au fur et à mesure le contenu du log, et ainsi je peux mettre en page dynamiquement ce contenu grâce à javascript et PHP.

À la place de simplement enregistrer l'url courante dans un fichier texte, je rajoute cette partie dans mon programme principal en Python (le script qui utilise Selenium) :

import json
import googleapiclient.discovery
import googleapiclient.errors

with open('API.txt', 'r', encoding="utf-8") as f:
    api_key = f.read().rstrip()

#authentification et configuration de l'API
api_service_name = "youtube"
api_version = "v3"
max_results = 1

with open('API.txt', 'r', encoding="utf-8") as f:
    DEVELOPER_KEY = f.read().rstrip()

youtube = googleapiclient.discovery.build(
    api_service_name, api_version, developerKey=DEVELOPER_KEY)

#la requête à l'API en elle-même
request = youtube.videos().list(
    part="snippet,contentDetails,statistics",
    id="L_LUpnjgPso",
)
response = request.execute()['items'][0]

###supprimer les éléments inutiles pour alléger le fichier au maximum
supp = ['kind', 'etag', 'id']
for s in supp:
    del response[s]

supp2 = ['channelId', 'liveBroadcastContent', 'localized']
for s2 in supp2:
    del response['snippet'][s2]

supp3 = ['dimension', 'projection', 'contentRating', 'licensedContent']
for s3 in supp3:
    del response['contentDetails'][s3]

###trouver le thumbnail avec les plus grande définition pour chaque url
thumb_sizes = ['maxres', 'high', 'medium', 'standard', 'default']
for thumb in thumb_sizes:
    if thumb in response['snippet']['thumbnails']:
        ###on utilise la liste temp_list car on ne peut pas supprimer des éléments du dictionnaire directement pendant son itération
        temp_list = []
        for element in response['snippet']['thumbnails']:
            if thumb != element:
                temp_list.append(element)
        for ar in temp_list:
            del response['snippet']['thumbnails'][ar]
        break

###changer le categoryId par une chaîne de caractère compréhensible
with open('catégoriesVidéos.json', 'r', encoding='utf-8') as f:
    data = f.read()
    video_categories = json.loads(data)['items']

    for cat in video_categories:
        if response['snippet']['categoryId'] == cat['id']:
            response['snippet']['categoryId'] = cat['title']

###simplifier un peu le format de la durée de la vidéo
durée_simple = response['contentDetails']['duration'].lstrip('PT').lower()
response['contentDetails']['duration'] = durée_simple

###enregistrer le tout dans un fichier JSON
with open('result_API.json', 'a', encoding="utf8") as fp:
    json.dump(response, fp)
    fp.write('\n')

J'ai essayé de nettoyer au maximum le fichier JSON pour que ce soit le plus rapide possible lorsque le RPi va envoyer des données sur le serveur. Vu que je suis plus à l'aise en Python ça me permet aussi de ne pas galérer en javascript ou en PHP pour faire la même chose.

J'ai utilise cette fonction de l'API de YouTube pour récupérer le fichier catégoriesVidéos.json, qui me permet d'avoir la catégorie de la vidéo en français, au lieu d'un code à 2 chiffres.
J'ai quand même nettoyé un peu le fichier JSON pour avoir quelque chose de plus clair, et plus simple pour travailler.

Il ressemble à ça :

{
  "items": [{
      "id": "1",
      "title": "Films et animations"
    },
    {
      "id": "2",
      "title": "Auto/Moto"
    },
    {
      "id": "10",
      "title": "Musique"
    },
    {
      "id": "15",
      "title": "Animaux"
    },
    {
      "id": "17",
      "title": "Sport"
    },

On obtient alors pour chaque vidéo (à partir d'une url) ce genre d'objet JSON :

{
"snippet": 
  {"publishedAt": 
  "2016-10-02T14:05:46Z", 
  "title": "Fireplace 10 hours full HD", 
  "description": "Fireplace 10 hours full HD for a romantic moment.\n10 hours burning logs loop play.", 
  "thumbnails": 
    {"maxres": 
      {"url": "https://i.ytimg.com/vi/L_LUpnjgPso/maxresdefault.jpg", 
      "width": 1280,
      "height": 720}}, 
  "channelTitle": "Fireplace 10 hours", 
  "tags": 
    ["fireplace 10 hours", 
    "fireplaces", 
    "fireplace video hd", 
    "fireplace video", 
    "HD fireplace", 
    "fireplace burning", 
    "burning logs",   
    "fireplace"], 
    "categoryId": "Divertissement"}, 
"contentDetails": 
  {"duration": "10h1m26s", 
  "definition": "hd", 
  "caption": "false"}, 
"statistics": 
  {"viewCount": "21852887", 
  "likeCount": "55694", 
  "dislikeCount": "4133", 
  "favoriteCount": "0"
}}

Peut-être que toutes ces informations ne me serviront pas et que j'en enlèverais plus tard, pour réduire encore la taille des fichiers envoyés.

Affichage dans la page PHP

Pour afficher ces informations j'utilise PHP afin de créer une structure HTML :

    <?php
      $arr_JSON = [];
      $fh = fopen('/chemin/log_results.json', 'r');
      while ($line = fgets($fh)) {
        array_push($arr_JSON, json_decode($line, TRUE));
       }
      fclose($fh);

      //on inverse l'ordre de l'array pour afficher les dernières vidéos en haut de la page (comme un blog)
      $arr_JSON = array_reverse($arr_JSON);
      $thumb_res = ['maxres', 'high', 'medium', 'default'];
      foreach($arr_JSON as $JSON) {
        echo '<article class="post">';
          echo '<aside class="date">'.$JSON['datetime']['heure'].'</br>'.$JSON['datetime']['jour'].'</aside>';
          echo '<div class="vid">';
            echo '<p>
                  Je regarde une nouvelle vidéo. </br>
                  Le titre est <span class="titre">'.$JSON['snippet']['title'].'.</span></br>
                  Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'.</span>
                  </p>';
                  //trouve l'image dans la plus grande résolution possible
                  foreach ($thumb_res as $res) {
                    if (array_key_exists($res, $JSON['snippet']['thumbnails'])){
                      echo '<img class="hidden" src="'.$JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">';
                      break;
                    } else {
                      continue;
                    }
                  }
          echo '</div>';
        echo '</article>';
      }
    ?>

Sur chaque ligne du fichier JSON se trouvent les informations concernant une vidéo. Chacune de ces vidéos fera l'objet d'un "post" comme celui-ci :

Interface v1 post.png

Il contient des infos basiques (titre, durée, date à laquelle le bot l'a regardée).

Il faut désormais que je me penche sur la façon dont je vais analyser les métadonnées de chacune de ces vidéos pour les faire paraître dans l'interface.

Cependant toutes les métadonnées n'existent pas forcément pour chaque vidéo (certaines n'ont pas de tags par exemple) : il faut donc vérifier leur existence dans le tableau associatif crée à partir du log en JSON pour chaque vidéo. En PHP il y a deux manières de faire ça, et je vais donc utiliser isset() puisque je n'aurais pas de cas où le contenu de l'array sea "null".

  if (isset($JSON['snippet']['tags'])){
    foreach($JSON['snippet']['tags'] as $tag){
      echo '<p>'.$tag;'<p>';
    }
  }

J'ai fait un bout de code qui permet de comparer tous les tags dans les x derneiers éléments pour ressortir le plus récurrent et le poster dans le blog. Ça permet de se rendre compte périodiquement des récurrences ou des thématiques qui ressortent à l'échelle "locale" du blog.

  //s'il y a des tags, on les range dans une array pour les comparer
        if (isset($JSON['snippet']['tags'])){
          foreach ($JSON['snippet']['tags'] as $tag){
            array_push($compare_tags, $tag);
          }
        }

        if ($compteur%4 == 3){
          //compare les tags des dernières vidéos et en sort le plus récurrent
          $compare_tags = array_map('strtolower', $compare_tags);
          $compare_tags = array_count_values($compare_tags);
          arsort($compare_tags);
          $sorted_tags = array_keys($compare_tags);
          $sorted_keys = array_keys(array_flip($compare_tags));
          //affiche le tag le plus récurrent si il y a plus d'une occurence (et donc si c'est un minimum pertinent)
          if ($sorted_keys[0] > 1){
            echo '<article class="vid">';
            echo 'En ce moment j\'aime bien '.$sorted_tags[0].' !';
            echo '</article>';
            //vide l'array, permet de comparer seulement les x derniers éléments
            $compare_tags = [];
          }
        }
        echo $compteur%4;
        $compteur += 1;

Cette structure est applicable à plusieurs métadonnées, avec des échelles différentes : on peut choisir de comparer les durées des vidéos, et d'en faire un compte-rendu avec une période de 20 vidéos, avec les catégories toutes les 30 vidéos, ou encore avec le nombre de likes ou de vues tous les 5 vidéos, etc.

Ces périodes peuvent être variables pour amener un peu de souplesse : on pourrait écrire ça comme ça:

if ($compteur%15 == mt_rand(13, 15){
 //code    
}

La fonction mt_rand est apparemment plus rapide et "mieux aléatoire" que la fonction rand().

Le problème est que ces posts ne sont pas dans l'ordre que je souhaiterais. J'ai utilisé une astuce dans la boucle en inversant l'ordre de l'array, c'est-à-dire que la dernière vidéo vue est postée en premier dans le code HTML, ce qui la place en haut de la page. Le problème, c'est que l'analyse comparative des tags par exemple se fait après dans cette même boucle, et donc si je décide de poster un commentaire du style "En ce moment j'aime bien x sujet", celui-ci se retrouvera en 3e position en partant du haut de la page. Le sens d'exécution du code/d'affichage est l'inverse du sens de logique de post sur un blog (les actualités les plus récentes en haut de la navigation.).

Je dois rectifier cet ordre soit par du javascript qui vient déplacer le noeud au bon endroit du DOM, soit peut-être par des paramètres de flexbox-order un peu compliqués pour que le noeud crée après soit positionné avant.

En fait c'est très simple avec les flexbox ! Je peux enlever la ligne qui inverse l'ordre de l'array dans le code PHP.

Avec ce code CSS, les éléments générés dans l'ordre peuvent être affichés dans l'ordre inverse:

/*Cet élément HTML est le conteneur de tous les posts, le changement d'ordre s'applique à tous ses enfants*/
section#corps{
  display:flex;
  flex-direction: column-reverse;
}
Affichage v2

J'utilise des spans au lieu des divs pour faire en sorte que tous les messages se suivent.

  foreach($arr_JSON as $JSON) {
        echo '<div class="post hideable">';
          //afficher la date
          echo '<aside class="date">'.$JSON['datetime']['jour'].'</br>'.$JSON['datetime']['heure'].'</aside>';
          echo '<p>';
          echo '<span class="text">';
            echo 'Je regarde une nouvelle vidéo. Le titre est <a href="https://www.youtube.com/watch?v='.$JSON['id'].'" class="titre" target="_blank">'.$JSON['snippet']['title'].'</a>.
            Elle dure <span class="durée">'.$JSON['contentDetails']['duration'].'. </span> ';
          echo '</span>';

Pour les images je crée une autre section :

 //on affiche les images dans une section différente donc on refait une boucle
 foreach($arr_JSON as $images_JSON) {
   //trouver l'image dans la plus grande résolution possible et l'afficher
   foreach ($thumb_res as $res) {
     if (array_key_exists($res, $images_JSON['snippet']['thumbnails'])){
       echo '<div>
             <img class="hidden" src="'.$images_JSON['snippet']['thumbnails'][$res]['url'].'" alt="'.$res.'_img">
             </div>';
       break;
     } else {
       continue;
     }
   }
 }

Avec l'event "wheel" de javascript on peut récupérer n'importe quel mouvement de scroll, même s'il n'y a rien à scroller (alors que l'évènement "scroll" ne s'active que lorsque l'on doit défiler pour voir le reste du contenu). Le delta Y de cet évènement permet de savoir quand on va vers le haut ou vers le bas.

imgs[0].classList.toggle('hidden');
var number = 0;
var prev_number = 0;
window.addEventListener('wheel', function(event) {
  if (event.deltaY < 0) {
    //scrolling up
    prev_number = number;
    number = clamp(number - 1, 0, imgs.length - 1);
  } else if (event.deltaY > 0) {
    //scrolling down
    prev_number = number;
    number = clamp(number + 1, 0, imgs.length - 1);
  }
  console.log('number', number);
  imgs[number].style.display = 'inherit';
  if (number != prev_number) {
    imgs[prev_number].style.display = 'none';
  }
});

Et la fonction clamp, qui permet de restreindre un nombre entre deux valeurs (pour éviter que des indexs trop grands ou trop petits créent une IndexError).

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

J'essaie de compartimenter les différentes parties de l'interface (LIVE | TEXTE | IMAGES) en colonnes dont la taille peut être ajustée, afin de pouvoir se concentrer sur une partie en particulier par exemple. Pour cela j'utilise la librairie split.js.

PARTIE JS :

//#live et #texte sont pour l'instant les deux colonnes que l'on peut retailler
Split(['#live', '#texte'], {
  sizes: [20, 80],
  minSize: 100,
  gutterSize:2,
});

PARTIE CSS : Les colonnes retaillables ont la classe "split", et la gouttière ("gutter", "gutter-horizontal") est crée automatiquement entre les colonnes par la librairie. On peut changer la couleur ou mettre une image pour décorer cette gouttière.
Apparemment il faut que les colonnes aient une hauteur définie pour que la librairie fonctionne. Pour l'instant je leur ai donné une hauteur fixe car le reste ne fonctionnait pas.

.gutter {
    background-color: #eee;
    height:100%;
    top:4em;
}

.gutter.gutter-horizontal {
  float: left;
  background-color: black;
  cursor: ew-resize;
}

.split, .gutter.gutter-horizontal {
  height: 600px;
  overflow-x: hidden;
  overflow-y: auto;
}

J'utilise ensuite AJAX pour récupérer le dernier log avec javascript, ce qui me permet d'utiliser les valeurs de l'objet JSON pour calculer le temps d'attente pour la prochaine vidéo.

let requestURL = 'http://curlybraces.be/erg/2019-2020/portrait-robot/log_last.json';
let request = new XMLHttpRequest();
request.open('GET', requestURL);
request.responseType = 'json';
request.send();

var logLast;
request.onload = function() {
  logLast = request.response;
  document.getElementById('test').innerHTML = 'Prochaine vidéo dans '+logLast.contentDetails.duration;
};