<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>tutorial &amp;mdash; Love.Law.Robots. by Ang Hou Fu</title>
    <link>https://write.as/houfu/tag:tutorial</link>
    <description></description>
    <pubDate>Wed, 26 Aug 2026 02:36:20 +0000</pubDate>
    <image>
      <url>https://i.snap.as/jvl6aNdV.png</url>
      <title>tutorial &amp;mdash; Love.Law.Robots. by Ang Hou Fu</title>
      <link>https://write.as/houfu/tag:tutorial</link>
    </image>
    <item>
      <title>Running Bots in your Matrix Room using Maubot</title>
      <link>https://write.as/houfu/running-bots-in-your-matrix-room-using-maubot?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Two people seated at a table. conversation. A robot barista behind them. Friendly. Warm. Inviting.&#xA;&#xA;Since receiving support requests and development ideas all over the place for redlines, I decided to try running a #Matrix room to centralise some discussion and an informal venue to ask weird and “stupid” questions. Since I am in the mood for experimentation, I wanted to run robots in my room. For my redlines room, I wanted a robot to track the activity on the GitHub repo, and welcome new users by posting what is the purpose of the room etc.&#xA;&#xA;I was surprised to find very little documents or bots on how to run a matrix bot. I decided to go with maubot because I like the idea of plugins. While the interface and the docs leaves something to be desired, it’s actually relatively straightforward. Here’s a short write-up/#tutorial of how I did it in case it helps someone out there.&#xA;&#xA;!--more--&#xA;&#xA;Step 1: Start a maubot server&#xA;&#xA;You’re going to need a place where your bot could listen for events and react to them. I decided to go with my Contabo VPS which I used to run docker compose services.&#xA;&#xA;The documentation does not provide instructions on how to use docker compose with the #docker image, and the instructions on reverse proxy leaves out #traefik. Having skewered many services into docker compose yaml, I was unfazed.&#xA;&#xA;Add this to your docker compose file, and you will get maubot with traefik SSL.&#xA;&#xA;  maubot:&#xA;    image: dock.mau.dev/maubot/maubot:latest&#xA;    restart: always&#xA;    volumes:&#xA;      ./maubot:/data:z&#xA;    labels:&#xA;      traefik.backend=maubot&#xA;      traefik.http.routers.maubot.rule=Host(maubot.example.com)&#xA;      traefik.http.routers.maubot.tls=true&#xA;      traefik.http.routers.maubot.tls.certresolver=myresolver&#xA;      traefik.http.middlewares.maubot-redirect.redirectscheme.scheme=https&#xA;      traefik.http.middlewares.maubot-redirect.redirectscheme.permanent=true&#xA;      traefik.http.routers.maubot.middlewares=maubot-redirect&#xA;      traefik.http.services.maubot.loadbalancer.server.port=29316&#xA;&#xA;Remember you need to modify the example to your circumstances: 1) correct domain name for your server, 2) create a directory for the data  and 3) fix the reference to the SSL certificate resolver for traefik.&#xA;&#xA;Run docker compose up -d and docker should now pull the maubot image and start running it.&#xA;&#xA;At this point, you should go to the data directory you created for maubot and look for “config.yaml” file. You can look at all the settings and decide whether you need to change them. However, you need to do this now: add an admin user in the “admins” section of the file by providing a user name with a password as a value.&#xA;&#xA;Step 2: Create a new user to be the bot&#xA;&#xA;I wanted a special user to be the bot for my room, so I registered one in my home matrix server for this purpose. It’s like signing up for the service again. There’s nothing special in this step.&#xA;&#xA;You do need to take note of the “access token” for a future step. Find it in Element by pressing on the “Access Token” dropdown arrow under Settings -  Help &amp; About -  Advanced.&#xA;&#xA;Screenshot of access token screen.&#xA;&#xA;Step 3: Create a client on the Maubot Manager&#xA;&#xA;Now let’s dive into creating a bot.&#xA;&#xA;Assuming you’ve got the server up, you can now visit the Maubot manager by adding this “matrix/maubot” to your server’s path and logging in. You should now be able to see the Maubot Manager interface. It allows you to create instances and clients, as well as upload plugins. We’re going to do all three for the first bot we are creating. &#xA;&#xA;First, you’re going to need a client. This is actually the new user you created in step 2. Click on the plus button and add the required details.&#xA;&#xA;Screenshot of Maubot Manager &#xA;&#xA;You’re going to need the details of your bot user’s access token, user ID and homeserver. You can find the user ID in the Settings-General page and it looks like this format: @username:home server. My matrix ID for example is @houfu:matrix.esq.social. The other two information could be found in the screen highlighted in Step 2.&#xA;&#xA;It doesn’t appear to be necessary to fix a device id or avatar url to create your client. You can fill them in later.&#xA;&#xA;Step 4: Upload a plugin&#xA;&#xA;It’s time to get the code of the bot in.&#xA;&#xA;You can find a list of plugins available to maubot on this page: https://plugins.maubot.xyz/&#xA;&#xA;The plugins I chose were:&#xA;&#xA;Github bot to track the updates on the redlines repository.&#xA;A welcome bot&#xA;&#xA;In order to upload the plugin to your maubot server, you would upload an mbp file for the plugin in the Maubot Manager. In many Github repos, you can find a release with the mbp file you can download. Otherwise, you can clone the repo, install maubot through pip and then run \mbc build\ to get the mbp file.&#xA;&#xA;Use the Maubot Manager to upload the plugin.&#xA;&#xA;Step 5: Create an instance&#xA;&#xA;It’s time to bring the code to the bot. Create an instance and link the plugin to the client you created earlier.&#xA;&#xA;Once you have created an instance, you can edit its settings in the box provided. The settings are project specific, so be sure to check the documentation.&#xA;&#xA;Once you have reached this step and checked that everything is enabled and running, your bot is operational and ready for action! (You may need to follow some bot specific instruction like entering commands in your matrix room, so read the docs!)&#xA;&#xA;It works!&#xA;&#xA;Marvel at the wonders of automation!&#xA;&#xA;Screenshot of Element showing a bot in action in a matrix room&#xA;&#xA;Have fun with your bots!&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38_ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/running-bots-in-your-matrix-room-using-maubot&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/s--cD1SqLno--/f_auto,q_auto/v1/blog-images/vrcxnb9o7gjaz9vvsatv" alt="Two people seated at a table. conversation. A robot barista behind them. Friendly. Warm. Inviting."/></p>

<p>Since receiving support requests and development ideas all over the place for <a href="https://www.lovelawrobots.com/workshopping-my-little-story-about-redlines" rel="nofollow">redlines</a>, I decided to try running a <a href="https://write.as/houfu/tag:Matrix" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Matrix</span></a> room to centralise some discussion and an informal venue to ask weird and “stupid” questions. Since I am in the mood for experimentation, I wanted to run robots in my room. For my redlines room, I wanted a robot to track the activity on the GitHub repo, and welcome new users by posting what is the purpose of the room etc.</p>

<p>I was surprised to find very little documents or bots on how to run a matrix bot. I decided to go with <a href="https://github.com/maubot/maubot" rel="nofollow">maubot</a> because I like the idea of plugins. While the interface and the docs leaves something to be desired, it’s actually relatively straightforward. Here’s a short write-up/<a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> of how I did it in case it helps someone out there.</p>



<h3 id="step-1-start-a-maubot-server">Step 1: Start a maubot server</h3>

<p>You’re going to need a place where your bot could listen for events and react to them. I decided to go with my Contabo VPS which I used to run docker compose services.</p>

<p>The documentation does not provide instructions on how to use docker compose with the <a href="https://write.as/houfu/tag:docker" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docker</span></a> image, and the instructions on reverse proxy leaves out <a href="https://write.as/houfu/tag:traefik" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">traefik</span></a>. Having skewered many services into docker compose yaml, I was unfazed.</p>

<p>Add this to your docker compose file, and you will get maubot with traefik SSL.</p>

<pre><code class="language-yaml">  maubot:
    image: dock.mau.dev/maubot/maubot:latest
    restart: always
    volumes:
      - ./maubot:/data:z
    labels:
      - traefik.backend=maubot
      - traefik.http.routers.maubot.rule=Host(`maubot.example.com`)
      - traefik.http.routers.maubot.tls=true
      - traefik.http.routers.maubot.tls.certresolver=myresolver
      - traefik.http.middlewares.maubot-redirect.redirectscheme.scheme=https
      - traefik.http.middlewares.maubot-redirect.redirectscheme.permanent=true
      - traefik.http.routers.maubot.middlewares=maubot-redirect
      - traefik.http.services.maubot.loadbalancer.server.port=29316
</code></pre>

<p>Remember you need to modify the example to your circumstances: 1) correct domain name for your server, 2) create a directory for the data  and 3) fix the reference to the SSL certificate resolver for traefik.</p>

<p>Run <code>docker compose up -d</code> and docker should now pull the maubot image and start running it.</p>

<p>At this point, you should go to the data directory you created for maubot and look for “config.yaml” file. You can look at all the settings and decide whether you need to change them. However, you need to do this now: add an admin user in the “admins” section of the file by providing a user name with a password as a value.</p>

<h3 id="step-2-create-a-new-user-to-be-the-bot">Step 2: Create a new user to be the bot</h3>

<p>I wanted a special user to be the bot for my room, so I registered one in my home matrix server for this purpose. It’s like signing up for the service again. There’s nothing special in this step.</p>

<p>You do need to take note of the “access token” for a future step. Find it in Element by pressing on the “Access Token” dropdown arrow under Settings –&gt; Help &amp; About –&gt; Advanced.</p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/s--_9etdjk4--/f_auto,q_auto/v1/blog-images/wgpbe4qu4flrpwcandus" alt="Screenshot of access token screen."/></p>

<h3 id="step-3-create-a-client-on-the-maubot-manager">Step 3: Create a client on the Maubot Manager</h3>

<p>Now let’s dive into creating a bot.</p>

<p>Assuming you’ve got the server up, you can now visit the Maubot manager by adding this “_matrix/maubot” to your server’s path and logging in. You should now be able to see the Maubot Manager interface. It allows you to create instances and clients, as well as upload plugins. We’re going to do all three for the first bot we are creating.</p>

<p>First, you’re going to need a client. This is actually the new user you created in step 2. Click on the plus button and add the required details.</p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/s--ipZ5Gpx1--/f_auto,q_auto/v1/blog-images/uqqtf11wlpgco48jrp6p" alt="Screenshot of Maubot Manager "/></p>

<p>You’re going to need the details of your bot user’s access token, user ID and homeserver. You can find the user ID in the Settings-General page and it looks like this format: @<em>username:home server.</em> My matrix ID for example is @houfu:matrix.esq.social. The other two information could be found in the screen highlighted in Step 2.</p>

<p>It doesn’t appear to be necessary to fix a device id or avatar url to create your client. You can fill them in later.</p>

<h3 id="step-4-upload-a-plugin">Step 4: Upload a plugin</h3>

<p>It’s time to get the code of the bot in.</p>

<p>You can find a list of plugins available to maubot on this page: <a href="https://plugins.maubot.xyz/" rel="nofollow">https://plugins.maubot.xyz/</a></p>

<p>The plugins I chose were:</p>
<ul><li><a href="https://github.com/maubot/github" rel="nofollow">Github bot</a> to track the updates on the redlines repository.</li>
<li><a href="https://github.com/williamkray/maubot-welcome" rel="nofollow">A welcome bot</a></li></ul>

<p>In order to upload the plugin to your maubot server, you would upload an mbp file for the plugin in the Maubot Manager. In many Github repos, you can find a release with the mbp file you can download. Otherwise, you can clone the repo, install maubot through pip and then run `mbc build` to get the mbp file.</p>

<p>Use the Maubot Manager to upload the plugin.</p>

<h3 id="step-5-create-an-instance">Step 5: Create an instance</h3>

<p>It’s time to bring the code to the bot. Create an instance and link the plugin to the client you created earlier.</p>

<p>Once you have created an instance, you can edit its settings in the box provided. The settings are project specific, so be sure to check the documentation.</p>

<p>Once you have reached this step and checked that everything is enabled and running, your bot is operational and ready for action! (You may need to follow some bot specific instruction like entering commands in your matrix room, so read the docs!)</p>

<h3 id="it-works">It works!</h3>

<p>Marvel at the wonders of automation!</p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/s--du5d2VOD--/f_auto,q_auto/v1/blog-images/rzigid0xhjp3zpquf1dz" alt="Screenshot of Element showing a bot in action in a matrix room"/></p>

<p>Have fun with your bots!</p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/running-bots-in-your-matrix-room-using-maubot" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/running-bots-in-your-matrix-room-using-maubot</guid>
      <pubDate>Fri, 08 Sep 2023 01:13:33 +0000</pubDate>
    </item>
    <item>
      <title>[Part 5] Do more with docassemble: Provide an audio file for your user to download 💾</title>
      <link>https://write.as/houfu/part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Introduction&#xA;&#xA;You must have worked hard to get here! We are almost at the end now.&#xA;&#xA;Our journey took us from providing the user experience, figuring out what should happen in the background, and interacting with an external service. &#xA;&#xA;In this part, we ask docassemble to provide a file for the user to download.&#xA;&#xA;Provisioning a File&#xA;&#xA;When we left part 2, this was our result screen.&#xA;    &#xA;    event: finalscreen&#xA;    question: |&#xA;      Download your sound file here.&#xA;    subquestion: |&#xA;      The audio on your text has been generated.&#xA;      &#xA;      You can preview it here too.&#xA;      &#xA;      audio controls&#xA;       source type=&#34;audio/mpeg&#34;&#xA;       Your browser does not support playing audio.&#xA;      /audio&#xA;      &#xA;      Press Back above if you want to modify the settings and generate a new file,&#xA;      or click Restart below to begin a new request.&#xA;    buttons:&#xA;      Exit: exit&#xA;      Restart: restart&#xA;&#xA;There are two places where you need an audio file.&#xA;&#xA;  In the “question”, a link to the file is provided in “here” for download.&#xA;  The audio preview widget (the thing which you click to play) also needs a link to the file to function.&#xA;&#xA;Luckily for us, docassemble provides a straightforward way to deal with files on the server. Simply stated, create a variable of type DAFile to hold a reference to the file, save the data to the file and then use it for those links in the results screen.&#xA;&#xA;Let’s get started. Add this block to your main.yml file.&#xA;    &#xA;    ---&#xA;    objects:&#xA;      generated: DAFile&#xA;    ---&#xA;&#xA;This block creates an object called “generated”, which is a DAFile. Now your interview code can use “generated”.&#xA;&#xA;Add the new line in the mandatory block we created in Part 2.&#xA;    &#xA;    mandatory: True&#xA;    code: |&#xA;    # The next line is new&#xA;      generated.initialize(filename=&#34;output.mp3&#34;) &#xA;      ttstask&#xA;      if ttstask.ready():&#xA;        finalscreen&#xA;      else:&#xA;        waitingscreen&#xA;&#xA;This code initialises “generated” by getting the docassemble server to provision it. If you use “generated” before initialising it, docassemble raises an error. 👻 (You will only get this error if you use the DAFile to create a file)&#xA;&#xA;Now your background action needs access to “generated”. Pass it in a keyword parameter in the background action you created in Part 3.&#xA;    &#xA;    code: |&#xA;      ttstask = backgroundaction(&#xA;        &#39;bgtask&#39;, &#xA;        texttosynthesize=texttosynthesize, &#xA;        voice=voice, &#xA;        speakingrate=speakingrate, &#xA;        pitch=pitch,&#xA;    # This is the new keyword parameter&#xA;        file=generated  &#xA;      )&#xA;&#xA;Now that your background action has the file, use it to save the audio content. Add the new lines below to bgtask that you also created in Part 3.&#xA;    &#xA;    event: bgtask&#xA;    code: |&#xA;      audio = gettexttospeech(&#xA;        actionargument(&#39;texttosynthesize&#39;),&#xA;        actionargument(&#39;voice&#39;),&#xA;        actionargument(&#39;speakingrate&#39;),&#xA;        actionargument(&#39;pitch&#39;),&#xA;      )&#xA;    # The next three lines are new&#xA;      fileoutput = actionargument(&#39;file&#39;) &#xA;      fileoutput.write(audio, binary=True) &#xA;      fileoutput.commit() &#xA;      backgroundresponse()&#xA;&#xA;We assign the file to a new variable in the background task and then use it to write the audio (make sure it is in binary format as MP3s are not text). After that, commit the file to save it in the server or your external storage, depending on your configuration. (The above method are from DAFile. You can read more details about what they do and other methods here.)&#xA;&#xA;Now that the file is ready, we can plunk it into our results screen. We are providing URLs here so that your user can download them from the browser. If you used paths, that would not work because it is the server&#39;s file system. Modify the lines in the results screen block.&#xA;    &#xA;    event: finalscreen&#xA;    question: |&#xA;    # Modify the next line&#xA;      Download your sound file here}). &#xA;    subquestion: |&#xA;      The audio on your text has been generated.&#xA;      &#xA;      You can preview it here too.&#xA;      &#xA;      audio controls&#xA;    # Modify the next line&#xA;       source src=&#34;${generated.urlfor()}&#34; type=&#34;audio/mpeg&#34; &#xA;       Your browser does not support playing audio.&#xA;      /audio&#xA;      &#xA;      Press Back above if you want to modify the settings and generate a new file,&#xA;      or click Restart below to begin a new request.&#xA;    buttons:&#xA;      Exit: exit&#xA;      Restart: restart&#xA;&#xA;To get the URL for a DAFIle, use the urlfor method. This lets you have an address you can use for downloading or the web browser.&#xA;&#xA;Conclusion&#xA;&#xA;Congratulations! You are now ready to run the interview. Give it a go and see if you can download the audio of a text you would like spoken. (If you are still at the Playground, you can click “Save and Run” to ensure your work is safe and test it around a bit.)&#xA;&#xA;This Text to Speech docassemble interview is relatively straightforward to me. Nevertheless, its simplicity also showcases several functions which you may want to be familiar with. Hopefully, you now have an idea of dealing with external services. If you manage to hook up something interesting, please share it with the community!&#xA;&#xA;Bonus: Trapping errors and alerting the users&#xA;&#xA;The code so far is enough to provide users with hours of fun (hopefully not at your expense). However, there are edge cases which you should consider if you plan to make your interview more widely available.&#xA;&#xA;Firstly, while it&#39;s pretty clear in this tutorial that you should have updated your Configuration so that this interview can find your service account, this doesn&#39;t always happen for others. Admins might have overlooked it.&#xA;&#xA;Add this code as the first mandatory code block of main.yml (before the one we wrote in Part 3):&#xA;    &#xA;    mandatory: True&#xA;    code: |&#xA;      if getconfig(&#39;google&#39;) is None or &#39;tts service account&#39; not in getconfig(&#39;google&#39;):   &#xA;        if getconfig(&#39;error notification email&#39;) is not None:&#xA;          sendemail(to=getconfig(&#39;error notification email&#39;), &#xA;            subject=&#39;docassemble-Google TTS raised an error&#39;, &#xA;            body=&#39;You need to set service account credentials in your google configuration.&#39; )&#xA;        else:&#xA;          log(&#39;docassemble-Google TTS raised an error -- You need to set service account credentials in your google configuration.&#39;)&#xA;          &#xA;        message(&#39;Error: No service account for Google TTS&#39;, &#39;Please contact your administrator.&#39;)&#xA;&#xA;Take note that if you add more than one mandatory block, they are called in the order of their appearance in the interview file. So if you put this after the mandatory code block defining our processes, the process gets called before checking whether we should run this code in the first place. &#xA;&#xA;This code block does a few things. Firstly it checks whether there is a &#34;google&#34; directive or a &#34;tts service account&#34; directive in the &#34;google directive&#34;. If it doesn&#39;t find any tts service account information, it checks whether the admin has set an error notification email in the Configuration. If it does, the server will send an email to the admin email to report the issue. If it doesn&#39;t, it prints the error on docassemble.log, one of the logs in the server. (If the admin doesn&#39;t check his email or logs, I am unsure how we can help the admin.)&#xA;&#xA;This mandatory check before starting the interview is helpful to catch the most obvious error – no configuration. However, you can pass this check by putting nonsense in the &#34;tts service account&#34;. Google is not going to process this. There may be other errors, such as Google being offline. &#xA;&#xA;Narrowing down every possible error will be very challenging. Instead, we will make one crucial check: the code did save a file at the end of the process. Even if we aren&#39;t going to be able to tell the user what went wrong, at least we spared the user the confusion of finding out that there was no file to download.&#xA;&#xA;First, let&#39;s write the code that makes the check. Add this new code block.&#xA;    &#xA;    event: filecheck&#xA;    code: |&#xA;      path = generated.path()&#xA;      if not os.path.exists(path):&#xA;        if getconfig(&#39;error notification email&#39;) is not None:&#xA;          sendemail(to=getconfig(&#39;error notification email&#39;), &#xA;            subject=&#39;docassemble-Google TTS raised an error&#39;, &#xA;            body=&#39;No file was saved in this interview.&#39; )&#xA;        else:&#xA;          log(&#39;docassemble-Google TTS raised an error -- No audio file was saved in this interview.&#39;)&#xA;        message(&#39;Error: No audio file was saved&#39;, &#39;We are not sure why. Please try again. If the problem persists, contact your administrator.&#39;)&#xA;This code checks whether the audio file (generated, a DAFile) is an actual file or an apparition. If it doesn&#39;t exist, the admin receives a message. The user is also alerted to the failure. &#xA;&#xA;We would need to add a need directive to our results screen so that the check is made before the final screen to download the file is shown.&#xA;    &#xA;    event: finalscreen &#xA;    need:  # Add this line&#xA;      filecheck  # Add this line&#xA;    question: |&#xA;      Download your sound file here}).&#xA;    subquestion: |&#xA;      The audio on your text has been generated.&#xA;      &#xA;      You can preview it here too.&#xA;      &#xA;      audio controls&#xA;       source src=&#34;${generated.urlfor()}&#34; type=&#34;audio/mpeg&#34;&#xA;       Your browser does not support playing audio.&#xA;      /audio&#xA;      &#xA;      Press Back above if you want to modify the settings and generate a new file,&#xA;      or click Restart below to begin a new request.&#xA;    buttons:&#xA;      Exit: exit&#xA;      Restart: restart&#xA;We would also need to import the python os standard library to make the check on our system. Add this new block near the top of our main.yml file. &#xA;    &#xA;    imports:&#xA;      os.path&#xA;There you have it! The interview checks before you start whether there&#39;s a service account. It also checks before showing you the final screen whether your request succeeded and if an audio file is ready to download.&#xA;&#xA;👈🏻 Go to the previous part.&#xA;&#xA;☝🏻Return to the overview of this tutorial.&#xA;&#xA;#tutorial #Python #Programming #docassemble #Google #TTS #LegalTech&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/docassemble-googleTTS_pt5-1.png" alt="Feature image"/></p>

<h2 id="introduction">Introduction</h2>

<p>You must have worked hard to get here! We are almost at the end now.</p>

<p>Our journey took us from <a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow">providing the user experience</a>, figuring out <a href="part-3-do-more-with-docassemble-getting-work-done-in-a-background-action/" rel="nofollow">what should happen in the background</a>, and <a href="part-4-do-more-with-docassemble-calling-google-text-to-speech/" rel="nofollow">interacting with an external service</a>.</p>

<p>In this part, we ask docassemble to provide a file for the user to download.</p>

<h2 id="provisioning-a-file">Provisioning a File</h2>

<p>When we left part 2, this was our result screen.</p>

<pre><code class="language-yaml">    event: final_screen
    question: |
      Download your sound file here.
    subquestion: |
      The audio on your text has been generated.
      
      You can preview it here too.
      
      &lt;audio controls&gt;
       &lt;source type=&#34;audio/mpeg&#34;&gt;
       Your browser does not support playing audio.
      &lt;/audio&gt;
      
      Press `Back` above if you want to modify the settings and generate a new file,
      or click `Restart` below to begin a new request.
    buttons:
      - Exit: exit
      - Restart: restart
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-5-1.png" alt=""/></p>

<p>There are two places where you need an audio file.</p>
<ol><li>In the “question”, a link to the file is provided in “here” for download.</li>
<li>The audio preview widget (the thing which you click to play) also needs a link to the file to function.</li></ol>

<p>Luckily for us, docassemble provides a straightforward way to deal with files on the server. Simply stated, create a variable of type <code>DAFile</code> to hold a reference to the file, save the data to the file and then use it for those links in the results screen.</p>

<p>Let’s get started. Add this block to your <code>main.yml</code> file.</p>

<pre><code class="language-yaml">    ---
    objects:
      - generated: DAFile
    ---
</code></pre>

<p>This block creates an object called “generated”, which is a <code>DAFile</code>. Now your interview code can use “generated”.</p>

<p>Add the new line in the mandatory block we created in Part 2.</p>

<pre><code class="language-yaml">    mandatory: True
    code: |
    # The next line is new
      generated.initialize(filename=&#34;output.mp3&#34;) 
      tts_task
      if tts_task.ready():
        final_screen
      else:
        waiting_screen
</code></pre>

<p>This code initialises “generated” by getting the docassemble server to provision it. If you use “generated” before initialising it, docassemble raises an error. 👻 (You will only get this error if you use the DAFile to create a file)</p>

<p>Now your background action needs access to “generated”. Pass it in a keyword parameter in the background action you created in Part 3.</p>

<pre><code>    code: |
      tts_task = background_action(
        &#39;bg_task&#39;, 
        text_to_synthesize=text_to_synthesize, 
        voice=voice, 
        speaking_rate=speaking_rate, 
        pitch=pitch,
    # This is the new keyword parameter
        file=generated  
      )
</code></pre>

<p>Now that your background action has the file, use it to save the audio content. Add the new lines below to bg_task that you also created in Part 3.</p>

<pre><code>    event: bg_task
    code: |
      audio = get_text_to_speech(
        action_argument(&#39;text_to_synthesize&#39;),
        action_argument(&#39;voice&#39;),
        action_argument(&#39;speaking_rate&#39;),
        action_argument(&#39;pitch&#39;),
      )
    # The next three lines are new
      file_output = action_argument(&#39;file&#39;) 
      file_output.write(audio, binary=True) 
      file_output.commit() 
      background_response()
</code></pre>

<p>We assign the file to a new variable in the background task and then use it to write the audio (make sure it is in binary format as MP3s are not text). After that, commit the file to save it in the server or your external storage, depending on your configuration. (The above method are from DAFile. You can read <a href="https://docassemble.org/docs/objects.html#DAFile" rel="nofollow">more details about what they do and other methods here</a>.)</p>

<p>Now that the file is ready, we can plunk it into our results screen. We are providing URLs here so that your user can download them from the browser. If you used paths, that would not work because it is the server&#39;s file system. Modify the lines in the results screen block.</p>

<pre><code class="language-yaml">    event: final_screen
    question: |
    # Modify the next line
      Download your sound file **[here](${generated.url_for(attachment=True)}).** 
    subquestion: |
      The audio on your text has been generated.
      
      You can preview it here too.
      
      &lt;audio controls&gt;
    # Modify the next line
       &lt;source src=&#34;${generated.url_for()}&#34; type=&#34;audio/mpeg&#34;&gt; 
       Your browser does not support playing audio.
      &lt;/audio&gt;
      
      Press `Back` above if you want to modify the settings and generate a new file,
      or click `Restart` below to begin a new request.
    buttons:
      - Exit: exit
      - Restart: restart
</code></pre>

<p>To get the URL for a DAFIle, use the <code>url_for</code> method. This lets you have an address you can use for downloading or the web browser.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Congratulations! You are now ready to run the interview. Give it a go and see if you can download the audio of a text you would like spoken. (If you are still at the Playground, you can click “Save and Run” to ensure your work is safe and test it around a bit.)</p>

<p>This Text to Speech docassemble interview is relatively straightforward to me. Nevertheless, its simplicity also showcases several functions which you may want to be familiar with. Hopefully, you now have an idea of dealing with external services. If you manage to hook up something interesting, please share it with the community!</p>

<h2 id="bonus-trapping-errors-and-alerting-the-users">Bonus: Trapping errors and alerting the users</h2>

<p>The code so far is enough to provide users with hours of fun (hopefully not at your expense). However, there are edge cases which you should consider if you plan to make your interview more widely available.</p>

<p>Firstly, while it&#39;s pretty clear in this tutorial that you should have updated your Configuration so that this interview can find your service account, this doesn&#39;t always happen for others. Admins might have overlooked it.</p>

<p>Add this code as the first mandatory code block of <code>main.yml</code> (before the one we wrote in Part 3):</p>

<pre><code class="language-yaml">    mandatory: True
    code: |
      if get_config(&#39;google&#39;) is None or &#39;tts service account&#39; not in get_config(&#39;google&#39;):   
        if get_config(&#39;error notification email&#39;) is not None:
          send_email(to=get_config(&#39;error notification email&#39;), 
            subject=&#39;docassemble-Google TTS raised an error&#39;, 
            body=&#39;You need to set service account credentials in your google configuration.&#39; )
        else:
          log(&#39;docassemble-Google TTS raised an error -- You need to set service account credentials in your google configuration.&#39;)
          
        message(&#39;Error: No service account for Google TTS&#39;, &#39;Please contact your administrator.&#39;)
</code></pre>

<p>Take note that if you add more than one mandatory block, they are called in the order of their appearance in the interview file. So if you put this after the mandatory code block defining our processes, the process gets called <em>before</em> checking whether we should run this code in the first place.</p>

<p>This code block does a few things. Firstly it checks whether there is a “google” directive or a “tts service account” directive in the “google directive”. If it doesn&#39;t find any tts service account information, it checks whether the admin has set an error notification email in the Configuration. If it does, the server will send an email to the admin email to report the issue. If it doesn&#39;t, it prints the error on <code>docassemble.log</code>, one of the logs in the server. (If the admin doesn&#39;t check his email or logs, I am unsure how we can help the admin.)</p>

<p>This mandatory check before starting the interview is helpful to catch the most obvious error – no configuration. However, you can pass this check by putting nonsense in the “tts service account”. Google is not going to process this. There may be other errors, such as Google being offline.</p>

<p>Narrowing down every possible error will be very challenging. Instead, we will make one crucial check: the code did save a file at the end of the process. Even if we aren&#39;t going to be able to tell the user what went wrong, at least we spared the user the confusion of finding out that there was no file to download.</p>

<p>First, let&#39;s write the code that makes the check. Add this new code block.</p>

<pre><code class="language-yaml">    event: file_check
    code: |
      path = generated.path()
      if not os.path.exists(path):
        if get_config(&#39;error notification email&#39;) is not None:
          send_email(to=get_config(&#39;error notification email&#39;), 
            subject=&#39;docassemble-Google TTS raised an error&#39;, 
            body=&#39;No file was saved in this interview.&#39; )
        else:
          log(&#39;docassemble-Google TTS raised an error -- No audio file was saved in this interview.&#39;)
        message(&#39;Error: No audio file was saved&#39;, &#39;We are not sure why. Please try again. If the problem persists, contact your administrator.&#39;)
</code></pre>

<p>This code checks whether the audio file (generated, a DAFile) is an actual file or an apparition. If it doesn&#39;t exist, the admin receives a message. The user is also alerted to the failure.</p>

<p>We would need to add a need directive to our results screen so that the check is made before the final screen to download the file is shown.</p>

<pre><code class="language-yaml">    event: final_screen 
    need:  # Add this line
      - file_check  # Add this line
    question: |
      Download your sound file **[here](${generated.url_for(attachment=True)}).**
    subquestion: |
      The audio on your text has been generated.
      
      You can preview it here too.
      
      &lt;audio controls&gt;
       &lt;source src=&#34;${generated.url_for()}&#34; type=&#34;audio/mpeg&#34;&gt;
       Your browser does not support playing audio.
      &lt;/audio&gt;
      
      Press `Back` above if you want to modify the settings and generate a new file,
      or click `Restart` below to begin a new request.
    buttons:
      - Exit: exit
      - Restart: restart
</code></pre>

<p>We would also need to import the python os standard library to make the check on our system. Add this new block near the top of our <code>main.yml</code> file.</p>

<pre><code class="language-yaml">    imports:
      - os.path
</code></pre>

<p>There you have it! The interview checks before you start whether there&#39;s a service account. It also checks before showing you the final screen whether your request succeeded and if an audio file is ready to download.</p>

<p>👈🏻 <strong><a href="part-4-do-more-with-docassemble-calling-google-text-to-speech/" rel="nofollow">Go to the previous part.</a></strong></p>

<p>☝🏻<strong><a href="part-1-do-more-with-docassemble-google-text-to-speech/#a-roadmap-of-this-tutorial" rel="nofollow">Return to the overview of this tutorial.</a></strong></p>

<p><a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:Google" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Google</span></a> <a href="https://write.as/houfu/tag:TTS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">TTS</span></a> <a href="https://write.as/houfu/tag:LegalTech" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">LegalTech</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download</guid>
      <pubDate>Tue, 17 May 2022 15:44:34 +0000</pubDate>
    </item>
    <item>
      <title>[Part 4] Do more with docassemble: Calling Google Text To Speech 🎺</title>
      <link>https://write.as/houfu/part-4-do-more-with-docassemble-calling-google-text-to-speech?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Introduction&#xA;&#xA;So far, all our work is on our docassemble install, which has been quite a breeze. Now we come to the most critical part of this tutorial: working with an external service, Google Text to Speech. Different considerations come into play when working with others.&#xA;&#xA;In this part, we will install a client library from Google. We will then configure the setup to interact with Google’s servers and write this code in a separate module, googletts.py. At the end of this tutorial, your background action will be able to call the function gettexttospeech and get the audio file from Google. &#xA;&#xA;1\. A quick word about APIs&#xA;&#xA;The term “API” can be used loosely nowadays. Some people use it to describe how you can use a program or a software library. In this tutorial, an API refers to a connection between computer programs. Instead of a website or a desktop program, we’re using Python and docassemble to interact with Google Text to Speech. In some cases, like Google Text to Speech, an API is the only way to work with the program.&#xA;&#xA;The two most common ways to work with an API over the internet are (1) using a client library or (2) RESTful APIs. There are pros and cons to working with any one of these options. In this tutorial we are going to go with a client library that Google provided. This allows us to work with the API in a programming language we are familiar with, Python. RESTful APIs can have more support and features than a client language (especially if the programming language is not popular). Still, you’d need to know your way around the requests and similar packages if you want to use them in Python.&#xA;&#xA;2\. Install the Client Library in your docassemble&#xA;&#xA;Before we can start using the client library, we need to ensure that it’s there in our docassemble install. Programming in Python can be very challenging because of issues like this:&#xA;&#xA;source: https://imgs.xkcd.com/comics/pythonenvironment.png&#xA;&#xA;Luckily, you will not face this problem if you’re using docker for your docassemble install (which most people do). Do this instead:&#xA;&#xA;  Leave the Playground and go to another page called “Package Management”. (If you don’t see this page, you need to be either an admin or a developer)&#xA;  Under Install or update a package, specify google-cloud-texttospeech as the package to find on PyPI&#xA;  Click Update, and wait for the screen to show that the install is OK. (This takes time as there are quite a few dependencies to install)&#xA;  Verify that the google-cloud-texttospeech package has been installed by checking out the list of packages installed.&#xA;&#xA;video controls&#xA;source src=&#34;https://res.cloudinary.com/lovelawrobots/video/upload/fauto:video,qauto/v1/blog-videos/lije9wjf4tkd13jqkx4h&#34; type=&#34;video/mp4&#34;&#xA;Your browser does not support the video tag.&#xA;/video&#xA;&#xA;3\. Set up a Text To Speech service account in docassemble&#xA;&#xA;At this point, you should have obtained your Google Cloud Platform service account so that you can access the Text to Speech API. If you haven’t done so, please follow the instructions here. Take note that we will need your key information in JSON format. You don’t need to “Set your authentication environment variable” for this tutorial.&#xA;&#xA;If you have not realised it yet, the key information in JSON format is a secret. While Google’s Text to Speech platform has a generous free tier, the service is not free. So, expect to pay Google if somebody with access to your account tries to read The Lord of the Rings trilogy. In line with best practices, secrets should be kept in a private and secure place, which is not your code repository. Please don’t include your service account details in your playground files!&#xA;&#xA;Luckily, you can store this information in docassemble’s Configuration, which someone can’t access without an admin role and is not generally publicly available. Let’s do that by creating a directive google with a sub-directive of tts service account. Go to your configuration page and add these directives. Then fill out the information in JSON format you received from Google when you set up the service account.&#xA;&#xA;In this example, the lines you will add to the Configuration should look like lines 118 to 131.&#xA;&#xA;4\. Putting it all together in the googletts.py module&#xA;&#xA;Now that our environment is set up, it’s time to create our getspeechfromtext function.&#xA;&#xA;Head back to the Playground, Look for the dropdown titled “Folders”, click it, then select “Modules”.&#xA;&#xA;Look for the editor and rename the file as googletts.py. This is where you will enter the code to interact with Google Text to Speech. If you recall in part 3, we had left out a function named gettexttospeech. We were also supposed to feed it with the answers we collected from the interviews we wrote in part 2. Let’s enter the signature of the function now.&#xA;    &#xA;    def gettexttospeech(texttosynthesize, voice, speakingrate, pitch):&#xA;      //Enter more code here&#xA;      return&#xA; &#xA;Since our task is to convert text to speech, we can follow the code in the example provided by Google.&#xA;&#xA;A. Create the Google Text-to-Speech client&#xA;&#xA;Using the Python client library, we can create a client to interact with Google’s service.&#xA;&#xA;We need credentials to use the client to access the service. This is the secret you set up in step 3 above. It’s in docassemble’s configuration, under the directive google with a sub-directive of tts service account. Use docassemble’s getconfig to look into your configuration and get the secret tts service account as a JSON.&#xA;&#xA;With the secret to the service account, you can pass it to the class factory function and let it do the work.&#xA;    &#xA;    def gettexttospeech(texttosynthesize, voice, speakingrate, pitch):&#xA;        from google.cloud import texttospeech&#xA;        import json&#xA;        from docassemble.base.util import getconfig&#xA;    &#xA;        credentialinfo = json.loads(getconfig(&#39;google&#39;).get(&#39;tts service account&#39;), strict=False)&#xA;    &#xA;        client = texttospeech.TextToSpeechClient.fromserviceaccountinfo(credentialinfo)&#xA;&#xA;Now that the client is ready with your service account details, let&#39;s get some audio.&#xA;&#xA;B. Specify some options and submit the request&#xA;&#xA;The primary function to request Google to turn text into speech is synthesizespeech. The function needs a bunch of stuff — the text to convert, a set of voice options, and options for your audio file. Let’s create some with the answers to the questions in part 2. Add these lines of code to your function.&#xA;&#xA;The text to synthesise:&#xA;    &#xA;    inputtext = texttospeech.SynthesisInput(text=texttosynthesize)&#xA;&#xA;The voice options:&#xA;    &#xA;    voice = texttospeech.VoiceSelectionParams(&#xA;            languagecode=&#34;en-US&#34;,&#xA;            name=voice,&#xA;        )&#xA;  &#xA;The audio options:&#xA;    &#xA;    audioconfig = texttospeech.AudioConfig(&#xA;            audioencoding=texttospeech.AudioEncoding.MP3,&#xA;            speakingrate=speakingrate,&#xA;            pitch=pitch,&#xA;        )&#xA;&#xA;Note that we did not allow all the options to be customised by the user. You can go through the documentation yourself to figure out what options you need or don’t need to worry the user. If you think the user should have more options, you’re free to write your questions and modify the code.&#xA;&#xA;Finally, submit the request and return the audio.&#xA;    &#xA;    response = client.synthesizespeech(&#xA;            request={&#34;input&#34;: inputtext, &#34;voice&#34;: voice, &#34;audioconfig&#34;: audioconfig}&#xA;        )&#xA;    &#xA;    return response.audiocontent&#xA;&#xA;Voila! The client library could call Google using your credentials and get your personalised result.&#xA;&#xA;5\. Let’s go back to our interview&#xA;&#xA;Now that you have written your function, it’s time to let our interview know where to find it.&#xA;&#xA;Go back to the playground, and add this new block in your main.yml file.&#xA;    &#xA;    ---&#xA;    modules:&#xA;      .googletts&#xA;    ---&#xA;&#xA;This block tells the interview that some of our functions (specifically, the gettexttospeech function) is found in the googletts module.&#xA;&#xA;Conclusion&#xA;&#xA;At the end of this part, you have written your googletts.py module and included it in your main.yml. You should also know how to install your python package to docassemble and edit your configuration file.&#xA;&#xA;Well, that leaves us with only one more thing to do. We’ve got our audio content; now we just need to get it to the user. How do we do that? What’s that? DAFile? Find out in the next part.&#xA;&#xA;👉🏻 Go to the final part.&#xA;&#xA;👈🏻 Go back to the previous part.&#xA;&#xA;☝🏻 Check out the overview of this tutorial.&#xA;&#xA;#tutorial #docassemble #LegalTech #Google #TTS #Programming #Python&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38_ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/part-4-do-more-with-docassemble-calling-google-text-to-speech&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/docassemble-googleTTS_pt4-1.png" alt="Feature image"/></p>

<h2 id="introduction">Introduction</h2>

<p>So far, all our work is on our docassemble install, which has been quite a breeze. Now we come to the most critical part of this tutorial: working with an external service, Google Text to Speech. Different considerations come into play when working with others.</p>

<p>In this part, we will install a client library from Google. We will then configure the setup to interact with Google’s servers and write this code in a separate module, <code>google_tts.py</code>. At the end of this tutorial, your background action will be able to call the function <code>get_text_to_speech</code> and get the audio file from Google.</p>

<h2 id="1-a-quick-word-about-apis">1. A quick word about APIs</h2>

<p>The term “API” can be used loosely nowadays. Some people use it to describe how you can use a program or a software library. In this tutorial, an API refers to <a href="https://en.wikipedia.org/wiki/API" rel="nofollow">a connection between computer programs</a>. Instead of a website or a desktop program, we’re using Python and docassemble to interact with Google Text to Speech. In some cases, like Google Text to Speech, an API is the only way to work with the program.</p>

<p>The two most common ways to work with an API over the internet are (1) using a client library or (2) RESTful APIs. There are pros and cons to working with any one of these options. In this tutorial we are going to go with a client library that Google provided. This allows us to work with the API in a programming language we are familiar with, Python. RESTful APIs can have more support and features than a client language (especially if the programming language is not popular). Still, you’d need to know your way around the requests and similar packages if you want to use them in Python.</p>

<h2 id="2-install-the-client-library-in-your-docassemble">2. Install the Client Library in your docassemble</h2>

<p>Before we can start using the client library, we need to ensure that it’s there in our docassemble install. Programming in Python can be very challenging because of issues like this:</p>

<p><img src="https://imgs.xkcd.com/comics/python_environment.png" alt=""/>
source: <a href="https://imgs.xkcd.com/comics/python_environment.png" rel="nofollow">https://imgs.xkcd.com/comics/python_environment.png</a></p>

<p>Luckily, you will not face this problem if you’re using docker for your docassemble install (which most people do). Do this instead:</p>
<ol><li>Leave the Playground and go to another page called “Package Management”. (If you don’t see this page, you need to be either an admin or a developer)</li>
<li>Under Install or update a package, specify <code>google-cloud-texttospeech</code> as the package to find on <strong>PyPI</strong></li>
<li>Click Update, and wait for the screen to show that the install is OK. (This takes time as there are quite a few dependencies to install)</li>
<li>Verify that the <code>google-cloud-texttospeech</code> package has been installed by checking out the list of packages installed.</li></ol>

<video controls="">
<source src="https://res.cloudinary.com/lovelawrobots/video/upload/f_auto:video,q_auto/v1/blog-videos/lije9wjf4tkd13jqkx4h" type="video/mp4">
Your browser does not support the video tag.
</video>

<h2 id="3-set-up-a-text-to-speech-service-account-in-docassemble">3. Set up a Text To Speech service account in docassemble</h2>

<p>At this point, you should have obtained your Google Cloud Platform service account so that you can access the Text to Speech API. If you haven’t done so, please follow the instructions <a href="https://cloud.google.com/text-to-speech/docs/before-you-begin" rel="nofollow">here</a>. Take note that we will need your key information in JSON format. You don’t need to “Set your authentication environment variable” for this tutorial.</p>

<p>If you have not realised it yet, the key information in JSON format is a <em>secret</em>. While Google’s Text to Speech platform has a generous free tier, the service is not free. So, expect to pay Google if somebody with access to your account tries to read <em>The Lord of the Rings</em> trilogy. In line with best practices, secrets should be kept in a private and secure place, which is not your code repository. Please don’t include your service account details in your playground files!</p>

<p>Luckily, you can store this information in docassemble’s Configuration, which someone can’t access without an admin role and is not generally publicly available. Let’s do that by creating a directive <code>google</code> with a sub-directive of <code>tts service account</code>. Go to your configuration page and add these directives. Then fill out the information in JSON format you received from Google when you set up the service account.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Screenshot-from-2022-05-14-21-44-28-1.png" alt=""/>
In this example, the lines you will add to the Configuration should look like lines 118 to 131.</p>

<h2 id="4-putting-it-all-together-in-the-google-tts-py-module">4. Putting it all together in the <code>google_tts.py</code> module</h2>

<p>Now that our environment is set up, it’s time to create our <code>get_speech_from_text</code> function.</p>

<p>Head back to the Playground, Look for the dropdown titled “Folders”, click it, then select “Modules”.</p>

<p>Look for the editor and rename the file as <code>google_tts.py</code>. This is where you will enter the code to interact with Google Text to Speech. If you recall <a href="part-3-do-more-with-docassemble-getting-work-done-in-a-background-action/#4-perform-the-background-action" rel="nofollow">in part 3</a>, we had left out a function named <code>get_text_to_speech</code>. We were also supposed to feed it with the answers we collected from the interviews we wrote in <a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow">part 2</a>. Let’s enter the signature of the function now.</p>

<pre><code class="language-python">    def get_text_to_speech(text_to_synthesize, voice, speaking_rate, pitch):
      //Enter more code here
      return
</code></pre>

<p>Since our task is to convert text to speech, we can follow the code in <a href="https://cloud.google.com/text-to-speech/docs/create-audio-text-client-libraries#create_audio_data" rel="nofollow">the example provided by Google</a>.</p>

<h3 id="a-create-the-google-text-to-speech-client">A. Create the Google Text-to-Speech client</h3>

<p>Using the Python client library, we can create a client to interact with Google’s service.</p>

<p>We need credentials to use the client to access the service. This is the secret you set up in step 3 above. It’s in docassemble’s configuration, under the directive <code>google</code> with a sub-directive of <code>tts service account</code>. Use docassemble’s <code>get_config</code> to look into your configuration and get the secret <code>tts service account</code> as a JSON.</p>

<p>With the secret to the service account, you can pass it to the class factory function and let it do the work.</p>

<pre><code class="language-python">    def get_text_to_speech(text_to_synthesize, voice, speaking_rate, pitch):
        from google.cloud import texttospeech
        import json
        from docassemble.base.util import get_config
    
        credential_info = json.loads(get_config(&#39;google&#39;).get(&#39;tts service account&#39;), strict=False)
    
        client = texttospeech.TextToSpeechClient.from_service_account_info(credential_info)
</code></pre>

<p>Now that the client is ready with your service account details, let&#39;s get some audio.</p>

<h3 id="b-specify-some-options-and-submit-the-request">B. Specify some options and submit the request</h3>

<p>The primary function to request Google to turn text into speech is <code>synthesize_speech</code>. The function needs a bunch of stuff — the text to convert, a set of voice options, and options for your audio file. Let’s create some with the answers to the questions in part 2. Add these lines of code to your function.</p>

<p>The text to synthesise:</p>

<pre><code class="language-python">    input_text = texttospeech.SynthesisInput(text=text_to_synthesize)
</code></pre>

<p>The voice options:</p>

<pre><code class="language-python">    voice = texttospeech.VoiceSelectionParams(
            language_code=&#34;en-US&#34;,
            name=voice,
        )
</code></pre>

<p>The audio options:</p>

<pre><code class="language-python">    audio_config = texttospeech.AudioConfig(
            audio_encoding=texttospeech.AudioEncoding.MP3,
            speaking_rate=speaking_rate,
            pitch=pitch,
        )
</code></pre>

<p>Note that we did not allow all the options to be customised by the user. You can go through the <a href="https://cloud.google.com/python/docs/reference/texttospeech/latest/google.cloud.texttospeech_v1.types" rel="nofollow">documentation</a> yourself to figure out what options you need or don’t need to worry the user. If you think the user should have more options, you’re free to write your questions and modify the code.</p>

<p>Finally, submit the request and return the audio.</p>

<pre><code class="language-python">    response = client.synthesize_speech(
            request={&#34;input&#34;: input_text, &#34;voice&#34;: voice, &#34;audio_config&#34;: audio_config}
        )
    
    return response.audio_content
</code></pre>

<p>Voila! The client library could call Google using your credentials and get your personalised result.</p>

<h2 id="5-let-s-go-back-to-our-interview">5. Let’s go back to our interview</h2>

<p>Now that you have written your function, it’s time to let our interview know where to find it.</p>

<p>Go back to the playground, and add this new block in your <code>main.yml</code> file.</p>

<pre><code class="language-yaml">    ---
    modules:
      - .google_tts
    ---
</code></pre>

<p>This block tells the interview that some of our functions (specifically, the <code>get_text_to_speech</code> function) is found in the <code>google_tts</code> module.</p>

<h2 id="conclusion">Conclusion</h2>

<p>At the end of this part, you have written your <code>google_tts.py</code> module and included it in your <code>main.yml</code>. You should also know how to install your python package to docassemble and edit your configuration file.</p>

<p>Well, that leaves us with only one more thing to do. We’ve got our audio content; now we just need to get it to the user. How do we do that? What’s that? <code>DAFile</code>? Find out in the next part.</p>

<p>👉🏻 <strong><a href="part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download/" rel="nofollow">Go to the final part.</a></strong></p>

<p>👈🏻 <strong><a href="part-3-do-more-with-docassemble-getting-work-done-in-a-background-action/" rel="nofollow">Go back to the previous part</a>.</strong></p>

<p>☝🏻<a href="part-1-do-more-with-docassemble-google-text-to-speech/#a-roadmap-of-this-tutorial" rel="nofollow"> <strong>Check out the overview of this tutorial.</strong></a></p>

<p><a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:LegalTech" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">LegalTech</span></a> <a href="https://write.as/houfu/tag:Google" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Google</span></a> <a href="https://write.as/houfu/tag:TTS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">TTS</span></a> <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/part-4-do-more-with-docassemble-calling-google-text-to-speech" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/part-4-do-more-with-docassemble-calling-google-text-to-speech</guid>
      <pubDate>Mon, 16 May 2022 15:12:14 +0000</pubDate>
    </item>
    <item>
      <title>[Part 3] Do more with docassemble: Getting work done in a background action</title>
      <link>https://write.as/houfu/part-3-do-more-with-docassemble-getting-work-done-in-a-background-action?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Introduction&#xA;&#xA;In Part 2, we managed to write a few questions and a result screen. Even with all that eye candy, you will notice that you can’t run this interview. There is no mandatory block, so docassemble does not know what it needs to do to run the interview. In this part, I will talk about the code block required to run the interview, forming the foundation of our next steps.&#xA;&#xA;1\. The backbone of this interview&#xA;&#xA;In ordinary docassemble interviews, your endpoint is a template or a form. For this interview, the endpoint is a sound file. So let’s start with this code block. It tells the reader how the interview will run. Since it is a pretty important block, we should put it near the top of the interview file, maybe right under the meta block. (In this tutorial, the order of blocks does not affect how the interview will run. Or at least until you get to the bonus section.)&#xA;    &#xA;    mandatory: True&#xA;    code: |&#xA;      ttstask&#xA;      finalscreen&#xA;&#xA;If you recall, the user downloads the audio file in the final screen.&#xA;&#xA;So this mandatory code block asks docassemble to “do” the ttstask and then show the final screen. Now we need to define ttstask, and your interview will be ready to run.&#xA;&#xA;So what should ttstask be? The most straightforward answer is that it is the result of the API call to create the sound file. You can write a code block that gets and returns a sound file and assigns it to ttstask.&#xA;&#xA;Well, don’t write that code block yet.&#xA;&#xA;2\. Introducing the background action&#xA;&#xA;If you call an API on the other side of the internet, you should know that many things can happen along the way. For example, it takes time for your request to reach Google, then for Google to process it using its fancy robots, send the result back to your server, and then for your server to send it back to the user. In my experience, Google’s and docassemble’s latency is quite good, but it is still noticeable with large requests.&#xA;&#xA;A user is not supposed to notice that a program lags when the interview runs well. If the user realises that the interview is stuck on the same page for too long, the user might get worried that the interview is broken. The truth is that we are waiting for the file to come back. Get back in your chair and wait for it!&#xA;&#xA;To improve user experience, you should have a waiting screen where you tell the user to hold his horses. While this happens, the interview should work in the background. In this manner, your user is assured everything is well while your interview focuses on getting the file back from Google.&#xA;&#xA;docassemble already provides a mechanism for the docassemble program to carry out background actions. It’s aptly called backgroundaction().&#xA;&#xA;Check out a sample of a background action by reading the first example block (”Return a value”) under the Background processes category. Modify our mandatory code block by following the mandatory code block in the example. It should look like this.&#xA;    &#xA;    mandatory: True&#xA;    code: |&#xA;      ttstask&#xA;      if ttstask.ready():&#xA;        finalscreen&#xA;      else:&#xA;        waitingscreen&#xA;&#xA;So now we tell docassemble to do the ttstask, our background task. Once it is ready, show the final screen. If the task is not ready, show the waiting screen.&#xA;&#xA;3\. Define the background action&#xA;&#xA;Now that we have defined the interview flow, it’s time to do the background action. In the spirit of docassemble, we do this by defining ttstask.&#xA;&#xA;The next code block defines the task. Adapt this example into our interview file as follows.&#xA;    &#xA;    code: |&#xA;      ttstask = backgroundaction(&#xA;        &#39;bgtask&#39;, &#xA;        texttosynthesize=texttosynthesize, &#xA;        voice=voice, &#xA;        speakingrate=speakingrate, &#xA;        pitch=pitch,&#xA;      )&#xA;&#xA;So we have defined ttstask as a background action. The background action function has two kinds of arguments.&#xA;&#xA;The first positional argument (&#34;bgtask”) is the name of the code block that the background action should execute in the background.&#xA;&#xA;The other keyword arguments are the information you need to pass to this background action like the texttosynthesize, voice etc. These options you answered earlier during this interview will now be used for this background action. Defining your variables here in a mandatory block indirectly also ensures that docassemble will look for the answers for these variables before performing this code block.&#xA;&#xA;So why do you need to define all the variables in this way? Don’t forget that the background action is a separate process from the rest of your interview so they don’t share the same variables. To enable these processes to share their information, you pass on the variables from the main interview process to the background action.&#xA;&#xA;4\. Perform the background action&#xA;&#xA;We have defined the background action. Now let’s code what happens inside the background action.&#xA;&#xA;The background action is defined in an event called bgtask. Now add a new code block as follows:&#xA;    &#xA;    event: bgtask&#xA;    code: |&#xA;      audio = gettexttospeech(&#xA;        actionargument(&#39;texttosynthesize&#39;),&#xA;        actionargument(&#39;voice&#39;),&#xA;        actionargument(&#39;speakingrate&#39;),&#xA;        actionargument(&#39;pitch&#39;),&#xA;      )&#xA;      backgroundresponse()&#xA;&#xA;So in this code block, we say that the audio is obtained by calling a function named gettexttospeech. For gettexttospeech to produce an audio file, it requires the answers to the questions you asked the user earlier. As a background process, it gets access to the variables you defined earlier through the keywords of the backgroundaction function by calling actionargument.&#xA;&#xA;Once gettexttospeech is completed, we call backgroundresponse(). Calling backgroundresponse is important for a background action as it tells docassemble that this is the endpoint for the background action. Make sure you don’t leave your background action without it.&#xA;&#xA;5\. Provide a waiting screen&#xA;&#xA;Before we leave the example block for background processes, let’s add the question block that tells the user to wait for their audio file. Find the block which defines waitingscreen, and adapt it for your interview as follows.&#xA;    &#xA;    event: waitingscreen&#xA;    question: |&#xA;      Hang tight.&#xA;      Google is doing its magic.&#xA;    subquestion: |&#xA;      This screen will reload every&#xA;      few seconds until the file&#xA;      is available.&#xA;    reload: True&#xA;&#xA;By adding reload: True to the block, you tell docassemble to refresh the screen every 10 seconds. This helps the user to believe that they only need to be patient and some “magic” is going on somewhere.&#xA;&#xA;Conclusion&#xA;&#xA;In the next part of the tutorial, we will dive into gettexttospeech. (What else, right?) We will need to call Google’s Text-to-Speech API to do this. If you found it easy to follow the code blocks in this part of the tutorial, we will kick this up a notch — the next file we will be working on ends with a “.py”.&#xA;&#xA;👉🏻 Go ahead to the next part&#xA;&#xA;👈🏻 Go to the previous part&#xA;&#xA;👈🏻 Check out the overview of this tutorial.&#xA;&#xA;#tutorial #docassemble #Programming #Python #Google #TTS&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/part-3-do-more-with-docassemble-getting-work-done-in-a-background-action&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/docassemble-googleTTS_pt3-1.png" alt="Feature image"/></p>

<h2 id="introduction">Introduction</h2>

<p>In <a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow">Part 2</a>, we managed to write a few questions and a result screen. Even with all that eye candy, you will notice that you can’t run this interview. There is no <code>mandatory</code> block, so docassemble does not know what it needs to do to run the interview. In this part, I will talk about the code block required to run the interview, forming the foundation of our next steps.</p>

<h2 id="1-the-backbone-of-this-interview">1. The backbone of this interview</h2>

<p>In ordinary docassemble interviews, your endpoint is a template or a form. For this interview, the endpoint is a sound file. So let’s start with this code block. It tells the reader how the interview will run. Since it is a pretty important block, we should put it near the top of the interview file, maybe right under the meta block. (In this tutorial, the order of blocks does not affect how the interview will run. Or at least until you get to the bonus section.)</p>

<pre><code class="language-yaml">    mandatory: True
    code: |
      tts_task
      final_screen
</code></pre>

<p>If you recall, the user downloads the audio file in the <code>final screen</code>.</p>

<p>So this mandatory code block asks docassemble to “do” the <code>tts_task</code> and then show the final screen. Now we need to define <code>tts_task</code>, and your interview will be ready to run.</p>

<p>So what should <code>tts_task</code> be? The most straightforward answer is that it is the result of the API call to create the sound file. You can write a code block that gets and returns a sound file and assigns it to <code>tts_task</code>.</p>

<p>Well, don’t write that code block yet.</p>

<h2 id="2-introducing-the-background-action">2. Introducing the background action</h2>

<p>If you call an API on the other side of the internet, you should know that many things can happen along the way. For example, it takes time for your request to reach Google, then for Google to process it using its fancy robots, send the result back to your server, and then for your server to send it back to the user. In my experience, Google’s and docassemble’s latency is quite good, but it is still noticeable with large requests.</p>

<p>A user is not supposed to notice that a program lags when the interview runs well. If the user realises that the interview is stuck on the same page for too long, the user might get worried that the interview is broken. The truth is that we are waiting for the file to come back. Get back in your chair and wait for it!</p>

<p>To improve user experience, you should have a waiting screen where you tell the user to hold his horses. While this happens, the interview should work in the background. In this manner, your user is assured everything is well while your interview focuses on getting the file back from Google.</p>

<p>docassemble already provides a mechanism for the docassemble program to carry out background actions. It’s <a href="https://docassemble.org/docs/background.html#background_action" rel="nofollow">aptly called <code>background_action()</code></a>.</p>

<p>Check out a sample of a background action by reading the first example block (”Return a value”) under the Background processes category. Modify our mandatory code block by following the mandatory code block in the example. It should look like this.</p>

<pre><code class="language-yaml">    mandatory: True
    code: |
      tts_task
      if tts_task.ready():
        final_screen
      else:
        waiting_screen
</code></pre>

<p>So now we tell docassemble to do the <code>tts_task</code>, our background task. Once it is ready, show the final screen. If the task is not ready, show the waiting screen.</p>

<h2 id="3-define-the-background-action">3. Define the background action</h2>

<p>Now that we have defined the interview flow, it’s time to do the background action. In the spirit of docassemble, we do this by defining <code>tts_task</code>.</p>

<p>The next code block defines the task. Adapt this example into our interview file as follows.</p>

<pre><code>    code: |
      tts_task = background_action(
        &#39;bg_task&#39;, 
        text_to_synthesize=text_to_synthesize, 
        voice=voice, 
        speaking_rate=speaking_rate, 
        pitch=pitch,
      )
</code></pre>

<p>So we have defined <code>tts_task</code> as a background action. The background action function has two kinds of arguments.</p>

<p>The first positional argument (“bg_task”) is the name of the code block that the background action should execute in the background.</p>

<p>The other keyword arguments are the information you need to pass to this background action like the <code>text_to_synthesize</code>, <code>voice</code> etc. These options you answered earlier during this interview will now be used for this background action. Defining your variables here in a mandatory block indirectly also ensures that docassemble will look for the answers for these variables before performing this code block.</p>

<p>So why do you need to define all the variables in this way? Don’t forget that the background action is a separate process from the rest of your interview so they don’t share the same variables. To enable these processes to share their information, you pass on the variables from the main interview process to the background action.</p>

<h2 id="4-perform-the-background-action">4. Perform the background action</h2>

<p>We have defined the background action. Now let’s code what happens inside the background action.</p>

<p>The background action is defined in an event called <code>bg_task</code>. Now add a new code block as follows:</p>

<pre><code>    event: bg_task
    code: |
      audio = get_text_to_speech(
        action_argument(&#39;text_to_synthesize&#39;),
        action_argument(&#39;voice&#39;),
        action_argument(&#39;speaking_rate&#39;),
        action_argument(&#39;pitch&#39;),
      )
      background_response()
</code></pre>

<p>So in this code block, we say that the audio is obtained by calling a function named <code>get_text_to_speech</code>. For <code>get_text_to_speech</code> to produce an audio file, it requires the answers to the questions you asked the user earlier. As a background process, it gets access to the variables you defined earlier through the keywords of the <code>background_action</code> function by calling <code>action_argument</code>.</p>

<p>Once <code>get_text_to_speech</code> is completed, we call <code>background_response()</code>. Calling background_response is important for a background action as it tells docassemble that this is the endpoint for the background action. Make sure you don’t leave your background action without it.</p>

<h2 id="5-provide-a-waiting-screen">5. Provide a waiting screen</h2>

<p>Before we leave the example block for background processes, let’s add the question block that tells the user to wait for their audio file. Find the block which defines <code>waiting_screen</code>, and adapt it for your interview as follows.</p>

<pre><code class="language-yaml">    event: waiting_screen
    question: |
      Hang tight.
      Google is doing its magic.
    subquestion: |
      This screen will reload every
      few seconds until the file
      is available.
    reload: True
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-4-1.png" alt=""/></p>

<p>By adding <code>reload: True</code> to the block, you tell docassemble to refresh the screen every 10 seconds. This helps the user to believe that they only need to be patient and some “magic” is going on somewhere.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In the next part of the tutorial, we will dive into <code>get_text_to_speech</code>. (What else, right?) We will need to call Google’s Text-to-Speech API to do this. If you found it easy to follow the code blocks in this part of the tutorial, we will kick this up a notch — the next file we will be working on ends with a “.py”.</p>

<p>👉🏻 <strong><a href="part-4-do-more-with-docassemble-calling-google-text-to-speech/" rel="nofollow">Go ahead to the next part</a></strong></p>

<p>👈🏻<a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow"> <strong>Go to the previous part</strong></a></p>

<p>👈🏻 <strong><a href="part-1-do-more-with-docassemble-google-text-to-speech/#a-roadmap-of-this-tutorial" rel="nofollow">Check out the overview of this tutorial.</a></strong></p>

<p><a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:Google" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Google</span></a> <a href="https://write.as/houfu/tag:TTS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">TTS</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/part-3-do-more-with-docassemble-getting-work-done-in-a-background-action" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/part-3-do-more-with-docassemble-getting-work-done-in-a-background-action</guid>
      <pubDate>Mon, 16 May 2022 14:48:24 +0000</pubDate>
    </item>
    <item>
      <title>[Part 2] Do more with docassemble: Start a project and write a few questions</title>
      <link>https://write.as/houfu/part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Introduction&#xA;&#xA;In Part 1, we talked about what we will do and the things you need to follow in this tutorial. Let’s get our hands wet now!&#xA;&#xA;We are going to get the groundwork done by creating four pages. The first page gets the text to be turned into speech. The second page chooses the voice which Google will use to generate the audio. The third page edits some attributes in the production of the audio. The last page is the results page to download the spoken text.&#xA;&#xA;If you are familiar with docassemble, nothing here is exciting, so you can skip this part. If you’re very new to docassemble, this is a gentle way to introduce you to getting started.&#xA;&#xA;1\. All projects begin like this&#xA;&#xA;Log in to your docassemble program and go to the Playground. We will be doing most of the work here.&#xA;&#xA;If you’re working from a clean install, your screen probably looks like this.&#xA;&#xA;The default new project in docassemble&#39;s Playground.&#xA;&#xA;  Let’s change the name of the interview file from test.yml to main.yml.&#xA;  Delete all the default blocks/text in the interview file. We are going to replace it with the blocks for this project.&#xA;&#xA;You will have a clean main.yml file at the end.&#xA;&#xA;2\. I never Meta an Interview like you&#xA;&#xA;I like to start my interview file with a meta block that tells someone about this interview and who made it.&#xA;&#xA;It’s not easy to remember what a meta block looks like every time. You can use the example blocks in the playground to insert template blocks and modify them.&#xA;&#xA;The example blocks also link to the relevant part of the documentation for easy reference. (It’s the blue “View Documentation” button.)&#xA;&#xA;video controls&#xA;  source src=&#34;https://res.cloudinary.com/lovelawrobots/video/upload/fauto:video,qauto/v1/blog-videos/kebmt1lnqtemkvcrl2xf&#34; type=&#34;video/mp4&#34;&#xA;/video&#xA;&#xA;You should also use the example blocks as much as possible when you’re new to docassemble and writing YAML files. If you keep using those example blocks, you will not forget to separate your blocks with --- and you will minimise errors about indents and lists. After some practice (and lots of mistakes), you should be familiar with the syntax of a YAML file.&#xA;&#xA;So, even though the example blocks section is found below the fold, you should not leave home without it.&#xA;&#xA;You can write anything you like in the meta block as it’s a reference for other users. The field title, for example, is shown as the name of the interview on the “Available Interviews” page.&#xA;&#xA;For this project, this is the meta block I used.&#xA;    &#xA;    metadata:&#xA;      title: |&#xA;        Google TTS Interview&#xA;      short title: |&#xA;        Have Google read your text&#xA;      description: |&#xA;        This interview produces a sound file based &#xA;        on the text input by the user and other options.&#xA;      revisiondate: 2022-05-01&#xA;   &#xA;3\. Let’s write some questions&#xA;&#xA;This is probably the most visual part of the tutorial, so enjoy it!&#xA;&#xA;An easy way to think about question blocks is that they represent a page in your interview. As long as docassemble can find question blocks that answer all the variables it needs to finish the interview, you can organise and write your question block as you prefer.&#xA;&#xA;So, for example, you can add this text box block which asks you to provide the input text. You can find the example text box block under the Fields category. (Putting no label allows the block to appear as if only one variable is set in this question)&#xA;    &#xA;    question: |&#xA;      Tell me what text you would like Google to voice.&#xA;    fields:&#xA;      no label: texttosynthesize&#xA;        input type: area&#xA;      note: |&#xA;          The limit is 5000 characters. (Short paragraphs should be fine)&#xA;&#xA;You can also combine several questions on one page like this question for setting the audio options. Using the range slider example block under the Fields category, you can build this block.&#xA;    &#xA;     question: |&#xA;      Modify the way Google speaks your text.&#xA;    subquestion: |&#xA;      You can skip this if you don&#39;t need any modifications.&#xA;    fields:&#xA;      Speaking pitch: pitch&#xA;        datatype: range&#xA;        min: -20.0&#xA;        max: 20.0&#xA;        default: 0&#xA;      note: |&#xA;          20 means increase 20 semitones from the original pitch. &#xA;          -20 means decrease 20 semitones from the original pitch. &#xA;      Speaking rate/speed: speakingrate&#xA;        datatype: range&#xA;        min: 0.25&#xA;        max: 4.0&#xA;        default: 1.0&#xA;        step: 0.1&#xA;      note: |&#xA;          1.0 is the normal native speed supported by the specific voice. &#xA;          2.0 is twice as fast, and 0.5 is half as fast.&#xA;  &#xA;&#xA;Notice that I have set constraints and defaults in this block based on the documentation of the various options. This will help the user avoid pesky and demoralising error messages from the external API by entering unacceptable values.&#xA;&#xA;A common question for a newcomer is how should present a question to a user? You can use a list of choices like the one below. (Build this question using the Radio buttons example block under the Multiple Choice category.)&#xA;    &#xA;     question: |&#xA;      Choose the voice that Google will use.&#xA;    field: voice&#xA;    default: en-US-Wavenet-A&#xA;    choices:&#xA;      en-US-Wavenet-A&#xA;      en-US-Wavenet-B&#xA;      en-US-Wavenet-C&#xA;      en-US-Wavenet-D&#xA;      en-US-Wavenet-E&#xA;      en-US-Wavenet-F&#xA;      en-US-Wavenet-G&#xA;      en-US-Wavenet-H&#xA;      en-US-Wavenet-I&#xA;      en-US-Wavenet-J&#xA;    under: |&#xA;      You can preview the voices here.&#xA;&#xA;An interesting side question: When do I use a slider or a text entry box?&#xA;&#xA;It depends on the kind of information you want. If you input numbers, the field&#39;s datatype should be a number. If you’re making a choice, a list of options works better.&#xA;&#xA;Honestly, it takes some experience to figure out what works best. Think about all the online forms you have experienced and what you liked or did not like. To gain experience quickly, you can experiment by trying different fields in docassemble and asking yourself whether it gets the job done.&#xA;&#xA;4\. The Result Screen&#xA;&#xA;Now that you have asked all your questions, it’s time to give your user the answer.&#xA;&#xA;The result screen is shown when Google’s API has processed the user’s request and sent over the mp3 file containing the synthesised speech. In the result screen, you will be able to download the file. It’s also helpful to allow the user to preview the sound file so that the user can go back and modify any options.&#xA;    &#xA;    event: finalscreen&#xA;    question: |&#xA;      Download your sound file here.&#xA;    subquestion: |&#xA;      The audio on your text has been generated.&#xA;      &#xA;      You can preview it here too.&#xA;      &#xA;      audio controls&#xA;       source type=&#34;audio/mpeg&#34;&#xA;       Your browser does not support playing audio.&#xA;      /audio&#xA;      &#xA;      Press Back above if you want to modify the settings and generate a new file,&#xA;      or click Restart below to begin a new request.&#xA;    buttons:&#xA;      Exit: exit&#xA;      Restart: restart&#xA;&#xA;Note: This image shows the completed file with links on how to download it. The reference question block above does not contain any links.&#xA;&#xA;You would notice that I used an audio HTML tag in the subquestion to provide my media previewer. Take note that you can use HTML tags in your markdown text if docassemble does not have an option that meets your needs. However, your HTML hack might vary since this is based on the browser, so try to test as much as possible and avoid complex HTML.&#xA;&#xA;Preview: Let’s do some actual coding&#xA;&#xA;If you followed this tutorial carefully, your main.yml will have a meta block, 3 question blocks and one results screen. &#xA;&#xA;There are a few problems now:&#xA;&#xA;  You cannot run the interview. The main reason is that there’s no “mandatory” block, so docassemble does not know what it needs to execute to finish the job.&#xA;  The results screen does not contain a link to download or a media to preview.&#xA;  We haven’t even asked Google to provide us with a sound file.&#xA;&#xA;In the next part, we will go through the overall logic of the interview and do some actual coding. Once you are ready, head on over there!&#xA;&#xA;👉🏻 Head to the next part.&#xA;&#xA;👈🏻 Go back to the previous part.&#xA;&#xA;☝🏻 Check out the overview of this tutorial.&#xA;&#xA;#tutorial #docassemble #TTS #Google #Python #Programming&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/docassemble-googleTTS_pt2-1.png" alt="Feature image"/></p>

<h2 id="introduction">Introduction</h2>

<p>In <a href="part-1-do-more-with-docassemble-google-text-to-speech/" rel="nofollow">Part 1</a>, we talked about what we will do and the things you need to follow in this tutorial. Let’s get our hands wet now!</p>

<p>We are going to get the groundwork done by creating four pages. The first page gets the text to be turned into speech. The second page chooses the voice which Google will use to generate the audio. The third page edits some attributes in the production of the audio. The last page is the results page to download the spoken text.</p>

<p>If you are familiar with docassemble, nothing here is exciting, so you can skip this part. If you’re very new to docassemble, this is a gentle way to introduce you to getting started.</p>

<h2 id="1-all-projects-begin-like-this">1. All projects begin like this</h2>

<p>Log in to your docassemble program and go to the Playground. We will be doing most of the work here.</p>

<p>If you’re working from a clean install, your screen probably looks like this.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Screen-Shot-2022-05-08-at-23.16.05-1.png" alt="The default new project in docassemble&#39;s Playground."/></p>
<ol><li>Let’s change the name of the interview file from <code>test.yml</code> to <code>main.yml</code>.</li>
<li>Delete all the default blocks/text in the interview file. We are going to replace it with the blocks for this project.</li></ol>

<p>You will have a clean <code>main.yml</code> file at the end.</p>

<h2 id="2-i-never-meta-an-interview-like-you">2. I never Meta an Interview like you</h2>

<p>I like to start my interview file with a meta block that tells someone about this interview and who made it.</p>

<p>It’s not easy to remember what a meta block looks like every time. You can use <a href="https://docassemble.org/docs/playground.html#examples" rel="nofollow">the example blocks in the playground</a> to insert template blocks and modify them.</p>

<p>The example blocks also link to the relevant part of the documentation for easy reference. (It’s the blue “View Documentation” button.)</p>

<video controls="">
  <source src="https://res.cloudinary.com/lovelawrobots/video/upload/f_auto:video,q_auto/v1/blog-videos/kebmt1lnqtemkvcrl2xf" type="video/mp4">
</video>

<p>You should also use the example blocks as much as possible when you’re new to docassemble and writing YAML files. If you keep using those example blocks, you will not forget to separate your blocks with <code>---</code> and you will minimise errors about indents and lists. After some practice (and lots of mistakes), you should be familiar with the syntax of a YAML file.</p>

<p>So, even though the example blocks section is found below the fold, you should not leave home without it.</p>

<p>You can write anything you like in the meta block as it’s a reference for other users. The field title, for example, is shown as the name of the interview on the “Available Interviews” page.</p>

<p>For this project, this is the meta block I used.</p>

<pre><code class="language-yaml">    metadata:
      title: |
        Google TTS Interview
      short title: |
        Have Google read your text
      description: |
        This interview produces a sound file based 
        on the text input by the user and other options.
      revision_date: 2022-05-01
</code></pre>

<h2 id="3-let-s-write-some-questions">3. Let’s write some questions</h2>

<p>This is probably the most visual part of the tutorial, so enjoy it!</p>

<p>An easy way to think about question blocks is that they represent a page in your interview. As long as docassemble can find question blocks that answer all the variables it needs to finish the interview, you can organise and write your question block as you prefer.</p>

<p>So, for example, you can add this text box block which asks you to provide the input text. You can find the example text box block under the Fields category. (Putting <code>no label</code> allows the block to appear as if only one variable is set in this question)</p>

<pre><code class="language-yaml">    question: |
      Tell me what text you would like Google to voice.
    fields:
      - no label: text_to_synthesize
        input type: area
      - note: |
          The limit is 5000 characters. (Short paragraphs should be fine)
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-6.png" alt=""/></p>

<p>You can also combine several questions on one page like this question for setting the <a href="https://cloud.google.com/python/docs/reference/texttospeech/latest/google.cloud.texttospeech_v1.types.AudioConfig" rel="nofollow">audio options</a>. Using the range slider example block under the Fields category, you can build this block.</p>

<pre><code class="language-yaml">    question: |
      Modify the way Google speaks your text.
    subquestion: |
      You can skip this if you don&#39;t need any modifications.
    fields:
      - Speaking pitch: pitch
        datatype: range
        min: -20.0
        max: 20.0
        default: 0
      - note: |
          20 means increase 20 semitones from the original pitch. 
          -20 means decrease 20 semitones from the original pitch. 
      - Speaking rate/speed: speaking_rate
        datatype: range
        min: 0.25
        max: 4.0
        default: 1.0
        step: 0.1
      - note: |
          1.0 is the normal native speed supported by the specific voice. 
          2.0 is twice as fast, and 0.5 is half as fast.
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-1-1.png" alt=""/></p>

<p>Notice that I have set constraints and defaults in this block based on the documentation of the various options. This will help the user avoid pesky and demoralising error messages from the external API by entering unacceptable values.</p>

<p>A common question for a newcomer is how should present a question to a user? You can use a list of choices like the one below. (Build this question using the Radio buttons example block under the Multiple Choice category.)</p>

<pre><code class="language-yaml">    question: |
      Choose the voice that Google will use.
    field: voice
    default: en-US-Wavenet-A
    choices:
      - en-US-Wavenet-A
      - en-US-Wavenet-B
      - en-US-Wavenet-C
      - en-US-Wavenet-D
      - en-US-Wavenet-E
      - en-US-Wavenet-F
      - en-US-Wavenet-G
      - en-US-Wavenet-H
      - en-US-Wavenet-I
      - en-US-Wavenet-J
    under: |
      You can preview the voices [here](&lt;https://cloud.google.com/text-to-speech/docs/voices&gt;).
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-2-1.png" alt=""/></p>

<p>An interesting side question: When do I use a slider or a text entry box?</p>

<p>It depends on the kind of information you want. If you input numbers, the field&#39;s datatype should be a number. If you’re making a choice, a list of options works better.</p>

<p>Honestly, it takes some experience to figure out what works best. Think about all the online forms you have experienced and what you liked or did not like. To gain experience quickly, you can experiment by trying different fields in docassemble and asking yourself whether it gets the job done.</p>

<h2 id="4-the-result-screen">4. The Result Screen</h2>

<p>Now that you have asked all your questions, it’s time to give your user the answer.</p>

<p>The result screen is shown when Google’s API has processed the user’s request and sent over the mp3 file containing the synthesised speech. In the result screen, you will be able to download the file. It’s also helpful to allow the user to preview the sound file so that the user can go back and modify any options.</p>

<pre><code class="language-yaml">    event: final_screen
    question: |
      Download your sound file here.
    subquestion: |
      The audio on your text has been generated.
      
      You can preview it here too.
      
      &lt;audio controls&gt;
       &lt;source type=&#34;audio/mpeg&#34;&gt;
       Your browser does not support playing audio.
      &lt;/audio&gt;
      
      Press `Back` above if you want to modify the settings and generate a new file,
      or click `Restart` below to begin a new request.
    buttons:
      - Exit: exit
      - Restart: restart
</code></pre>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/image-3-1.png" alt=""/></p>

<p>Note: This image shows the <em>completed</em> file with links on how to download it. The reference question block above does not contain any links.</p>

<p>You would notice that I used an <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio" rel="nofollow">audio HTML tag</a> in the subquestion to provide my media previewer. Take note that you can use HTML tags in your markdown text if docassemble does not have an option that meets your needs. However, your HTML hack might vary since this is based on the browser, so try to test as much as possible and avoid complex HTML.</p>

<h2 id="preview-let-s-do-some-actual-coding">Preview: Let’s do some actual coding</h2>

<p>If you followed this tutorial carefully, your <code>main.yml</code> will have a meta block, 3 question blocks and one results screen.</p>

<p>There are a few problems now:</p>
<ul><li>You cannot run the interview. The main reason is that there’s no “mandatory” block, so docassemble does not know what it needs to execute to finish the job.</li>
<li>The results screen does not contain a link to download or a media to preview.</li>
<li>We haven’t even asked Google to provide us with a sound file.</li></ul>

<p>In the next part, we will go through the overall logic of the interview and do some actual coding. Once you are ready, head on over there!</p>

<p>👉🏻 <strong><a href="part-3-do-more-with-docassemble-getting-work-done-in-a-background-action/" rel="nofollow">Head to the next part.</a></strong></p>

<p>👈🏻 <strong><a href="part-1-do-more-with-docassemble-google-text-to-speech/" rel="nofollow">Go back to the previous part.</a></strong></p>

<p>☝🏻 <strong><a href="part-1-do-more-with-docassemble-google-text-to-speech/#a-roadmap-of-this-tutorial" rel="nofollow">Check out the overview of this tutorial.</a></strong></p>

<p><a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:TTS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">TTS</span></a> <a href="https://write.as/houfu/tag:Google" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Google</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions</guid>
      <pubDate>Mon, 16 May 2022 14:42:54 +0000</pubDate>
    </item>
    <item>
      <title>[Part 1] Do more with docassemble: Google Text to Speech</title>
      <link>https://write.as/houfu/part-1-do-more-with-docassemble-google-text-to-speech?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Most people associate docassemble with assembling documents using guided interviews. That’s in the name, right? The program asks a few questions and out pops a completed form, contract or document. However, the documentation makes it quite clear that docassemble can do more:&#xA;&#xA;  Though the name emphasizes the document assembly feature, docassemble interviews do not need to assemble a document; they might submit an application, direct the user to other resources on the internet, store user input, interact with APIs, or simply provide the user with information.&#xA;&#xA;In this post, let’s demonstrate how to use docassemble to call an API, get a response and provide it to a user. You can check out the completed code on Github (NB: the git branch I would recommend for following this post is blog. I am actively using this package, so I may add new features to the main branch that I don’t discuss here.)&#xA;&#xA;Problem Statement&#xA;&#xA;I do a lot of internal training on various legal and compliance topics. I think I am a pretty all right speaker, but I have my limitations — I can’t give presentations 24/7, and my performance varies in a particular session. Wouldn’t it be nice if I could give a presentation at any time engagingly and consistently?&#xA;&#xA;I could record my voice, but I did not like the result.&#xA;&#xA;I decided to use a text-to-speech program instead, like the one provided by Google Cloud Platform. I created a computerised version of my speech in the presentation. My audience welcomed this version as it was more engaging than a plain PowerPoint presentation. Staff whose first language was not (Singapore) English also found the voice clear and understandable.&#xA;&#xA;The original code was terminal based. I detailed my early exploits in this blog post last year. The script was great for developing something fast. However, as more of my colleagues became interested in incorporating such speech in their presentations, I needed something more user-friendly.&#xA;&#xA;I already have a docassemble installation at work, so it appears convenient to work on that. The program would have to do the following:&#xA;&#xA;  Ask the user what text it wants to transform into speech&#xA;  Allow the user to modify some properties of the speech (speed, pitch etc.)&#xA;  Call Google TTS API, grab the sound file and provide it to the user to download&#xA;&#xA;video controls  &#xA;source src=&#34;https://res.cloudinary.com/lovelawrobots/video/upload/fauto:video,qauto/v1/blog-videos/mdzntfhjg4srzizlwz3j&#34; type=&#34;video/mp4&#34;&#xA;Your browser does not support the video tag.&#xA;/video&#xA;&#xA;Assumptions&#xA;&#xA;To follow this tutorial, you will need the following:&#xA;&#xA;  A working docassemble install. You can start up an instance on your laptop by following these instructions.&#xA;  A Google Cloud Platform (GCP) account with a service account enabled for Google TTS. You can follow Google’s instructions here to set one up.&#xA;  Use the Playground provided in docassemble. If you&#39;d like to use an IDE, you can, but I wouldn’t be providing instructions like creating files to follow a docassemble package&#39;s directory structure.&#xA;  Some basic knowledge about docassemble. I wouldn’t be going through in detail how to write a block. If you can follow the Hello World example, you should have sufficient knowledge to follow this tutorial.&#xA;&#xA;A Roadmap of this Tutorial&#xA;&#xA;In the next part of this post, I talk about the thinking behind creating this interview and how I got the necessary information (off the web) to make it.&#xA;&#xA;In Part 2, we get the groundwork done by creating four pages. This provides us with a visual idea of what happens in this interview. &#xA;&#xA;In Part 3, I talk about docassemble&#39;s background action and why we should use it for this interview. Merging the visual requirements with code gives us a clearer picture of what we need to write.&#xA;&#xA;In Part 4, we work with an external API by using a client library for Python. We install this client library in our docassemble&#39;s python environment and write a python module.&#xA;&#xA;In Part 5, we finish the interview by coding the end product: an audio file in the guise of a DAFile. You can run the interview and get your text transformed into speech now! I also give some ideas of what else you might want to do in the project. &#xA;&#xA;Part 1: Familiarise yourself with the requirements&#xA;&#xA;To write a docassemble interview, it makes sense to develop it backwards. In a simple case, you would like docassemble to fill in a form. So you would get a form, figure out its requirements, and then write questions for each requirement.&#xA;&#xA;An API call is not a contract or a form, but your process is the same.&#xA;&#xA;Based on Google’s quickstart, this is the method in the Python library which synthesises speech.&#xA;    &#xA;     # Set the text input to be synthesized&#xA;        synthesisinput = texttospeech.SynthesisInput(text=&#34;Hello, World!&#34;)&#xA;    &#xA;    # Build the voice request, select the language code (&#34;en-US&#34;) and the ssml&#xA;    # voice gender (&#34;neutral&#34;)&#xA;        voice = texttospeech.VoiceSelectionParams(&#xA;            languagecode=&#34;en-US&#34;, &#xA;            ssmlgender=texttospeech.SsmlVoiceGender.NEUTRAL&#xA;        )&#xA;    &#xA;    # Select the type of audio file you want returned&#xA;        audioconfig = texttospeech.AudioConfig(&#xA;            audioencoding=texttospeech.AudioEncoding.MP3&#xA;        )&#xA;    &#xA;    # Perform the text-to-speech request on the text input with the selected&#xA;    # voice parameters and audio file type&#xA;        response = client.synthesizespeech(&#xA;            input=synthesisinput, voice=voice, audioconfig=audioconfig&#xA;        )&#xA;&#xA;From this example code, you need to provide the program with the input text (synthesis input), the voice, and audio configuration options to synthesise speech.&#xA;&#xA;That looks pretty straightforward, so you might be tempted to dive into it immediately.&#xA;&#xA;However, I would recommend going through the documents provided online.&#xA;&#xA;  docassemble provides some of the most helpful documentation, great for varying proficiency levels.&#xA;  Google’s Text To Speech’s documentation is more typical of a product offered by a big tech company. Demos, use cases and guides help you get started quickly. You’re going to have to dig deep to find the one for Python. It receives less love than the other programming languages.&#xA;&#xA;Reading the documentation, especially if you want to use a third-party service, is vital to know what’s available and how to exploit it fully. For example, going through the docs is the best way to find out what docassemble is capable of and learn about existing features — such as transforming a python list of strings into a human-readable list complete with an “and”.&#xA;&#xA;You don’t have to follow the quickstart if it does not meet your use case. Going through the documentation, I figured out that I wanted to give the user a choice of which voice to use rather than letting Google select that for me. Furthermore, audio options like how fast a speaker is will be handy since non-native listeners may appreciate slower speaking. Also, I don’t think I need the user to select a specific file format as mp3s should be fine.&#xA;&#xA;Let’s move on!&#xA;&#xA;This was a pretty short one. I hope I got you curious and excited about what comes next. Continue to the next part, where we get started on a project!&#xA;&#xA;👉🏻 Head to the next part of this tutorial!&#xA;&#xA;#tutorial #docassemble #Python #Programming #TTS #Google&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/part-1-do-more-with-docassemble-google-text-to-speech&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/docassemble-googleTTS-1.png" alt="Feature image"/></p>

<p>Most people associate docassemble with assembling documents using guided interviews. That’s in the name, right? The program asks a few questions and out pops a completed form, contract or document. However, <a href="https://docassemble.org/" rel="nofollow">the documentation</a> makes it quite clear that docassemble can do more:</p>

<blockquote><p>Though the name emphasizes the document assembly feature, <strong>docassemble</strong> interviews do not need to assemble a document; they might submit an application, direct the user to other resources on the internet, <a href="https://docassemble.org/docs/functions.html#storage" rel="nofollow">store</a> user input, interact with <a href="https://docassemble.org/docs/functions.html#google%20sheets%20example" rel="nofollow">APIs</a>, or simply provide the user with information.</p></blockquote>

<p>In this post, let’s demonstrate how to use docassemble to call an API, get a response and provide it to a user. You can check out the completed code on <a href="https://github.com/houfu/docassemble-googleTTS/tree/blog" rel="nofollow">Github</a> (NB: the git branch I would recommend for following this post is <code>blog</code>. I am actively using this package, so I may add new features to the main branch that I don’t discuss here.)</p>

<h2 id="problem-statement">Problem Statement</h2>

<p>I do a lot of internal training on various legal and compliance topics. I think I am a pretty all right speaker, but I have my limitations — I can’t give presentations 24/7, and my performance varies in a particular session. Wouldn’t it be nice if I could give a presentation at any time engagingly and consistently?</p>

<p>I could record my voice, but I did not like the result.</p>

<p>I decided to use a text-to-speech program instead, like the one provided by Google Cloud Platform. I created a computerised version of my speech in the presentation. My audience welcomed this version as it was more engaging than a plain PowerPoint presentation. Staff whose first language was not (Singapore) English also found the voice clear and understandable.</p>

<p>The <a href="https://github.com/houfu/TTS-experiments" rel="nofollow">original code</a> was terminal based. I detailed my early exploits in <a href="let-the-robots-do-the-talking-exploring-tts/" rel="nofollow">this blog post last year</a>. The script was great for developing something fast. However, as more of my colleagues became interested in incorporating such speech in their presentations, I needed something more user-friendly.</p>

<p>I already have a docassemble installation at work, so it appears convenient to work on that. The program would have to do the following:</p>
<ul><li>Ask the user what text it wants to transform into speech</li>
<li>Allow the user to modify some properties of the speech (speed, pitch etc.)</li>
<li>Call Google TTS API, grab the sound file and provide it to the user to download</li></ul>

<video controls="">
<source src="https://res.cloudinary.com/lovelawrobots/video/upload/f_auto:video,q_auto/v1/blog-videos/mdzntfhjg4srzizlwz3j" type="video/mp4">
Your browser does not support the video tag.
</video>

<h2 id="assumptions">Assumptions</h2>

<p>To follow this tutorial, you will need the following:</p>
<ul><li>A working docassemble install. You can start up an instance on your laptop by following <a href="https://docassemble.org/docs/docker.html#starting" rel="nofollow">these instructions</a>.</li>
<li>A Google Cloud Platform (GCP) account with a service account enabled for Google TTS. You can <a href="https://cloud.google.com/text-to-speech/docs/create-audio-text-command-line" rel="nofollow">follow Google’s instructions here</a> to set one up.</li>
<li>Use the <a href="https://docassemble.org/docs/playground.html" rel="nofollow">Playground</a> provided in docassemble. If you&#39;d like to use an IDE, you can, but I wouldn’t be providing instructions like creating files to follow a docassemble package&#39;s directory structure.</li>
<li>Some basic knowledge about docassemble. I wouldn’t be going through in detail how to write a block. If you can follow the <a href="https://docassemble.org/docs/helloworld.html" rel="nofollow">Hello World example</a>, you should have sufficient knowledge to follow this tutorial.</li></ul>

<h2 id="a-roadmap-of-this-tutorial">A Roadmap of this Tutorial</h2>

<p>In the next part of this post, I talk about the thinking behind creating this interview and how I got the necessary information (off the web) to make it.</p>

<p>In <a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow">Part 2</a>, we get the groundwork done by creating four pages. This provides us with a visual idea of what happens in this interview.</p>

<p>In <a href="part-3-do-more-with-docassemble-getting-work-done-in-a-background-action/" rel="nofollow">Part 3</a>, I talk about docassemble&#39;s background action and why we should use it for this interview. Merging the visual requirements with code gives us a clearer picture of what we need to write.</p>

<p>In <a href="part-4-do-more-with-docassemble-calling-google-text-to-speech/" rel="nofollow">Part 4</a>, we work with an external API by using a client library for Python. We install this client library in our docassemble&#39;s python environment and write a python module.</p>

<p>In <a href="part-5-do-more-with-docassemble-provide-an-audio-file-for-your-user-to-download/" rel="nofollow">Part 5</a>, we finish the interview by coding the end product: an audio file in the guise of a DAFile. You can run the interview and get your text transformed into speech now! I also give some ideas of what else you might want to do in the project.</p>

<h2 id="part-1-familiarise-yourself-with-the-requirements">Part 1: Familiarise yourself with the requirements</h2>

<p>To write a docassemble interview, it makes sense to develop it backwards. In a simple case, you would like docassemble to fill in a form. So you would get a form, figure out its requirements, and then write questions for each requirement.</p>

<p>An API call is not a contract or a form, but your process is the same.</p>

<p>Based on Google’s <a href="https://github.com/googleapis/python-texttospeech/blob/HEAD/samples/snippets/quickstart.py" rel="nofollow">quickstart</a>, this is the method in the Python library which synthesises speech.</p>

<pre><code class="language-python">    # Set the text input to be synthesized
        synthesis_input = texttospeech.SynthesisInput(text=&#34;Hello, World!&#34;)
    
    # Build the voice request, select the language code (&#34;en-US&#34;) and the ssml
    # voice gender (&#34;neutral&#34;)
        voice = texttospeech.VoiceSelectionParams(
            language_code=&#34;en-US&#34;, 
            ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL
        )
    
    # Select the type of audio file you want returned
        audio_config = texttospeech.AudioConfig(
            audio_encoding=texttospeech.AudioEncoding.MP3
        )
    
    # Perform the text-to-speech request on the text input with the selected
    # voice parameters and audio file type
        response = client.synthesize_speech(
            input=synthesis_input, voice=voice, audio_config=audio_config
        )
</code></pre>

<p>From this example code, you need to provide the program with the input text (synthesis input), the voice, and audio configuration options to synthesise speech.</p>

<p>That looks pretty straightforward, so you might be tempted to dive into it immediately.</p>

<p>However, I would recommend going through the documents provided online.</p>
<ul><li>docassemble provides some of the most helpful documentation, great for varying proficiency levels.</li>
<li>Google’s Text To Speech’s documentation is more typical of a product offered by a big tech company. Demos, use cases and guides help you get started quickly. You’re going to have to <a href="https://cloud.google.com/python/docs/reference/texttospeech/latest" rel="nofollow">dig deep</a> to find the one for Python. It receives less love than the other programming languages.</li></ul>

<p>Reading the documentation, especially if you want to use a third-party service, is vital to know what’s available and how to exploit it fully. For example, going through the docs is the best way to find out what docassemble is capable of and learn about existing features — such as <a href="https://docassemble.org/docs/functions.html#comma_and_list" rel="nofollow">transforming a python list of strings into a human-readable list</a> complete with an “and”.</p>

<p>You don’t have to follow the quickstart if it does not meet your use case. Going through the documentation, I figured out that I wanted to give the user a choice of which voice to use rather than letting Google select that for me. Furthermore, audio options like how fast a speaker is will be handy since non-native listeners may appreciate slower speaking. Also, I don’t think I need the user to select a specific file format as mp3s should be fine.</p>

<h2 id="let-s-move-on">Let’s move on!</h2>

<p>This was a pretty short one. I hope I got you curious and excited about what comes next. Continue to the next part, where we get started on a project!</p>

<p>👉🏻 <strong><a href="part-2-do-more-with-docassemble-start-a-project-and-write-a-few-questions/" rel="nofollow">Head to the next part of this tutorial!</a></strong></p>

<p><a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:TTS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">TTS</span></a> <a href="https://write.as/houfu/tag:Google" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Google</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/part-1-do-more-with-docassemble-google-text-to-speech" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/part-1-do-more-with-docassemble-google-text-to-speech</guid>
      <pubDate>Sun, 15 May 2022 16:06:42 +0000</pubDate>
    </item>
    <item>
      <title>So what difference does it make? Drawing red lines with Python</title>
      <link>https://write.as/houfu/so-what-difference-does-it-make-drawing-red-lines-with-python?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Way back in December 2021, I caught wind of the 2020 Revised Edition of the statutes in Singapore law:&#xA;&#xA;https://twitter.com/agcsingapore/status/1473111278860357641&#xA;&#xA;The AGC highlighted that the revised legislation now uses “simpler language”. I was curious about this claim and looked over their list of changes. I was not very impressed with them.&#xA;&#xA;However, I did not want to rely only on my subjective intuition to make that conclusion. I wanted to test it using data science. This meant I had to compare text, calculate the changes&#39; readability statistics, and see what changed.&#xA;&#xA;!--more--&#xA;&#xA;As part of this project, I wanted to see the changes. This should be a piece of cake. After all, Microsoft Word lets you do this with a few clicks.&#xA;&#xA;Python’s difflib: Good for programmers, not so for lawyers&#xA;&#xA;I did not want to reinvent the wheel, which probably would require me to figure out how to implement something called the Levenshtein Difference or “gestalt pattern matching” as an algorithm🤯. Luckily for me, the standard Python library already has a module that allows you to compare text and create a human-readable representation of its changes. It’s called difflib.&#xA;&#xA;If you spend a lot of time programming with Python, you might have already seen it in action. Here’s a sample output from the documentation:&#xA;&#xA;[&#39;    1. Beautiful is better than ugly.\n&#39;,&#xA; &#39;-   2. Explicit is better than implicit.\n&#39;,&#xA; &#39;-   3. Simple is better than complex.\n&#39;,&#xA; &#39;+   3.   Simple is better than complex.\n&#39;,&#xA; &#39;?     ++\n&#39;,&#xA; &#39;-   4. Complex is better than complicated.\n&#39;,&#xA; &#39;?            ^                     ---- ^\n&#39;,&#xA; &#39;+   4. Complicated is better than complex.\n&#39;,&#xA; &#39;?           ++++ ^                      ^\n&#39;,&#xA; &#39;+   5. Flat is better than nested.\n&#39;]&#xA;&#xA;As you can see, the start of the lines are marked with - and + at the beginning shows the source and the target lines, while a ? line indicates the changes in the lines. It’s gorgeous and informative.&#xA;&#xA;Feeling awesome, I immediately passed my text into an innocent Differ object from the difflib library. Unfortunately, the results were poor.&#xA;&#xA;This is not pretty. Expand this to see how much so.&#xA;&#xA;source = &#39;Any person residing in a welfare home may, if so directed by the Director‑General, be removed from that welfare home and admitted to another welfare home.&#39;&#xA;&#xA;compare = &#39;Any person residing in a welfare home may, if so directed by the Director‑General, be removed from that welfare home and admitted to another welfare home.&#39;&#xA;&#xA;import difflib&#xA;&#xA;d = difflib.Differ()&#xA;&#xA;list(d.compare(source, compare))&#xA;&#xA;  &#xA;&#xA;Out[1]:&#xA;&#xA;[&#39; A&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; y&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; p&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; s&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; s&#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; g&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; w&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; l&#39;,&#xA;&#xA;&#39; f&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; y&#39;,&#xA;&#xA;&#39; ,&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; f&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; s&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; c&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; b&#39;,&#xA;&#xA;&#39; y&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; D&#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; c&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; ‑&#39;,&#xA;&#xA;&#39; G&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; l&#39;,&#xA;&#xA;&#39; ,&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; b&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; v&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; f&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; w&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; l&#39;,&#xA;&#xA;&#39; f&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; i&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; d&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; n&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; t&#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; w&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; l&#39;,&#xA;&#xA;&#39; f&#39;,&#xA;&#xA;&#39; a&#39;,&#xA;&#xA;&#39; r&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; &#39;,&#xA;&#xA;&#39; h&#39;,&#xA;&#xA;&#39; o&#39;,&#xA;&#xA;&#39; m&#39;,&#xA;&#xA;&#39; e&#39;,&#xA;&#xA;&#39; .&#39;]&#xA;&#xA;There is no difference between the two lines.&#xA;&#xA;The problem was that when I passed a string of text into Differ, it compared based on the characters. You could see this effect in the documentation example:&#xA;&#xA; &#39;-   4. Complex is better than complicated.\n&#39;,&#xA; &#39;?            ^                     ---- ^\n&#39;,&#xA; &#39;+   4. Complicated is better than complex.\n&#39;,&#xA; &#39;?           ++++ ^                      ^\n&#39;,&#xA;&#xA;Instead of detecting that the whole word had changed, difflib noticed that the first four letters were alike.&#xA;&#xA;A simple workaround is to provide Differ with lines instead of a single line of text. This was provided in the documentation by having the \\n (a newline character) as a line separator.&#xA;&#xA;However, trying to break up a single line into several lines doesn’t solve the problem. Once an addition or subtraction to a phrase is detected, the difference propagates itself downstream, and soon the sentences are so different the algorithm gives up.&#xA;&#xA;[&#39;- No person shall import or export dutiable goods or tranship goods of a class dutiable &#39;,&#xA; &#39;? ^^         ^^^^                                                              ---------\n&#39;,&#xA; &#39;+ A person must not import or export dutiable goods or tranship goods of a class &#39;,&#xA; &#39;? ^        ++ ^^^^^\n&#39;,&#xA; &#39;- on import except under and in accordance with such regulations or restrictions as are prescribed. &#39;,&#xA; &#39;?                                                                                       ------------\n&#39;,&#xA; &#39;+ dutiable on import except under and in accordance with such regulations or restrictions as are &#39;,&#xA; &#39;? +++++++++\n&#39;,&#xA; &#39;+ prescribed. &#39;]&#xA;&#xA;Once there is an addition or subtraction, the words are marked as changed when they are found in different parts, even though there was no change to the text.&#xA;&#xA;This isn’t difflib’s fault. If difflib’s focus is on source code, then programmers are discouraged from writing long code in a single line. (In PEP-8, a style guide for Python, the maximum line length is 79 characters) Reading words in a giant block of characters is the norm for lawyers. Most legislation I have encountered so far stretches dozens of words over a single sentence 😓.&#xA;&#xA;There’s also another problem anyway. Rightly or wrongly, lawyers don’t expect changes to be rendered this way. The most common method I have seen is using the most powerful LegalTech tool, Microsoft Word, to mark changes.&#xA;&#xA;Example of track changes in Microsoft Word.&#xA;&#xA;Introducing Redlines — a simple tweak does the trick&#xA;&#xA;So, unfortunately, I needed to do some actual programming. How would I produce a redline comparison out of two texts? Gleaning some experience from solving an earlier problem, I decided to go back to the first principles. What would I do if I were a lawyer trying to redline a document?&#xA;&#xA;It turned out to be quite simple — I would compare words and phrases, not characters.&#xA;&#xA;So, the trick is to tokenise the text first. Tokens, for the most part, are individual words in a sentence. After tokenising the text, you have a sequence of tokens instead of characters. I tried to do this using a regular expression, which worked OK in my opinion.&#xA;&#xA;import re&#xA;&#xA;tokenizer = re.compile(r&#34;((?:+|[().?!-])\s*)&#34;)&#xA;&#xA;def tokenizetext(text: str) -  list[str]:&#xA;&#x9;# This function uses re to convert a string into tokens.&#xA;    return re.findall(tokenizer, text)&#xA;&#xA;Once you have your sequences of tokens to compare, you can get difflibs to do the hard work for you.&#xA;&#xA;Reading the documentation and the source code of difflibs, I found that it provided a low-level SequenceMatcher class. When sequences are compared using that class, it can provide a list of changes needed to convert a source list of tokens to its result. So, breaking that down, you can produce a redline representation instead of the original difflib representation.&#xA;&#xA;    def outputmarkdown(self) -  str:&#xA;        &#34;&#34;&#34;Returns the delta in markdown format.&#34;&#34;&#34;&#xA;        result = []&#xA;        style = &#39;red&#39;&#xA;&#xA;        if self.options.get(&#39;markdownstyle&#39;):&#xA;            style = self.options[&#39;markdownstyle&#39;]&#xA;&#xA;        if style == &#39;none&#39;:&#xA;            mdstyles = {&#34;ins&#34;: (&#39;ins&#39;, &#39;ins&#39;), &#34;del&#34;: (&#39;del&#39;, &#39;del&#39;)}&#xA;        elif &#39;red&#39;:&#xA;            mdstyles = {&#34;ins&#34;: (&#39;span style=&#34;color:red;font-weight:700;&#34;&#39;, &#39;span&#39;),&#xA;                         &#34;del&#34;: (&#39;span style=&#34;color:red;font-weight:700;text-decoration:line-through;&#34;&#39;, &#39;span&#39;)}&#xA;&#xA;        for tag, i1, i2, j1, j2 in self.opcodes:&#xA;            if tag == &#39;equal&#39;:&#xA;                result.append(&#34;&#34;.join(self.seq1[i1:i2]))&#xA;            elif tag == &#39;insert&#39;:&#xA;                result.append(f&#34;{mdstyles&#39;ins&#39;}{&#39;&#39;.join(self.seq2j1:j2])}/{mdstyles[&#39;ins&#39;}&#34;)&#xA;            elif tag == &#39;delete&#39;:&#xA;                result.append(f&#34;{mdstyles&#39;del&#39;}{&#39;&#39;.join(self.seq1i1:i2])}/{mdstyles[&#39;del&#39;}&#34;)&#xA;            elif tag == &#39;replace&#39;:&#xA;                result.append(&#xA;                    f&#34;{mdstyles&#39;del&#39;}{&#39;&#39;.join(self.seq1i1:i2])}/{mdstyles[&#39;del&#39;}&#34;&#xA;                    f&#34;{mdstyles&#39;ins&#39;}{&#39;&#39;.join(self.seq2j1:j2])}/{mdstyles[&#39;ins&#39;}&#34;)&#xA;&#xA;        return &#34;&#34;.join(result)&#xA;&#xA;I liked the results — a simple markdown that can be rendered on any webpage.&#xA;&#xA;Output from my Jupyter Notebook showing a comparison between two sections of a law.&#xA;&#xA;I didn’t find anything on the Python Package Index that would have helped me produce a redline representation of text changes, so I published mine anyway. If you found it helpful, feel free to let me know!&#xA;&#xA;Conclusion: Batteries Included&#xA;&#xA;I have always been mystified that Python is described as “batteries included”. Indeed, in this particular case, I found that the standard module had a pretty good solution, and with some tweaks, I was able to make my own. I would never have been able to do this twenty years ago when there was hardly any “stack overflow”, open-source libraries or active developer communities. This also informs my views on what “No Code” should be — batteries included. It’s an exciting time to be involved in code and programming!&#xA;&#xA;#Programming #Python #redlines #DataScience #tutorial #MicrosoftWord&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/so-what-difference-does-it-make-drawing-red-lines-with-python&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/bitmap.png" alt="Feature image"/></p>

<p>Way back in December 2021, I caught wind of the 2020 Revised Edition of the statutes in Singapore law:</p>

<p><a href="https://twitter.com/agcsingapore/status/1473111278860357641" rel="nofollow">https://twitter.com/agcsingapore/status/1473111278860357641</a></p>

<p>The AGC highlighted that the <a href="https://www.agc.gov.sg/our-roles/drafter-of-laws/legislation-and-revisions" rel="nofollow">revised legislation now uses “simpler language”</a>. I was curious about this claim and looked over <a href="https://www-agc-gov-sg-admin.cwp.sg/docs/default-source/our-roles-documents/drafter-of-laws/list-of-standard-revision-changes.pdf" rel="nofollow">their list of changes</a>. I was not very impressed with them.</p>

<p>However, I did not want to rely only on my subjective intuition to make that conclusion. I wanted to test <a href="evaluating-legislation-for-readability-exploring-plus" rel="nofollow">it using </a><em><a href="evaluating-legislation-for-readability-exploring-plus" rel="nofollow">data science</a></em>. This meant I had to compare text, calculate the changes&#39; readability statistics, and see what changed.</p>



<p>As part of this project, I wanted to see the changes. This should be a piece of cake. After all, Microsoft Word lets you do this with a few clicks.</p>

<h2 id="python-s-difflib-good-for-programmers-not-so-for-lawyers">Python’s difflib: Good for programmers, not so for lawyers</h2>

<p>I did not want to reinvent the wheel, which probably would require me to figure out how to implement something called the <a href="https://en.wikipedia.org/wiki/Levenshtein_distance" rel="nofollow">Levenshtein Difference</a> or “gestalt pattern matching” as an algorithm🤯. Luckily for me, the standard Python library already has a module that allows you to compare text and create a human-readable representation of its changes. It’s called <a href="https://docs.python.org/3/library/difflib.html" rel="nofollow">difflib</a>.</p>

<p>If you spend a lot of time programming with Python, you might have already seen it in action. Here’s a sample output from the <a href="https://docs.python.org/3/library/difflib.html#differ-example" rel="nofollow">documentation</a>:</p>

<pre><code>[&#39;    1. Beautiful is better than ugly.\n&#39;,
 &#39;-   2. Explicit is better than implicit.\n&#39;,
 &#39;-   3. Simple is better than complex.\n&#39;,
 &#39;+   3.   Simple is better than complex.\n&#39;,
 &#39;?     ++\n&#39;,
 &#39;-   4. Complex is better than complicated.\n&#39;,
 &#39;?            ^                     ---- ^\n&#39;,
 &#39;+   4. Complicated is better than complex.\n&#39;,
 &#39;?           ++++ ^                      ^\n&#39;,
 &#39;+   5. Flat is better than nested.\n&#39;]
</code></pre>

<p>As you can see, the start of the lines are marked with <code>-</code> and <code>+</code> at the beginning shows the source and the target lines, while a <code>?</code> line indicates the changes in the lines. It’s gorgeous and informative.</p>

<p>Feeling awesome, I immediately passed my text into an innocent <code>Differ</code> object from the difflib library. Unfortunately, the results were poor.</p>

<h4 id="this-is-not-pretty-expand-this-to-see-how-much-so">This is not pretty. Expand this to see how much so.</h4>

<pre><code>
source = &#39;Any person residing in a welfare home may, if so directed by the Director‑General, be removed from that welfare home and admitted to another welfare home.&#39;

compare = &#39;Any person residing in a welfare home may, if so directed by the Director‑General, be removed from that welfare home and admitted to another welfare home.&#39;

import difflib

d = difflib.Differ()

list(d.compare(source, compare))

  


Out[1]:

[&#39; A&#39;,

&#39; n&#39;,

&#39; y&#39;,

&#39; &#39;,

&#39; p&#39;,

&#39; e&#39;,

&#39; r&#39;,

&#39; s&#39;,

&#39; o&#39;,

&#39; n&#39;,

&#39; &#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; s&#39;,

&#39; i&#39;,

&#39; d&#39;,

&#39; i&#39;,

&#39; n&#39;,

&#39; g&#39;,

&#39; &#39;,

&#39; i&#39;,

&#39; n&#39;,

&#39; &#39;,

&#39; a&#39;,

&#39; &#39;,

&#39; w&#39;,

&#39; e&#39;,

&#39; l&#39;,

&#39; f&#39;,

&#39; a&#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; h&#39;,

&#39; o&#39;,

&#39; m&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; m&#39;,

&#39; a&#39;,

&#39; y&#39;,

&#39; ,&#39;,

&#39; &#39;,

&#39; i&#39;,

&#39; f&#39;,

&#39; &#39;,

&#39; s&#39;,

&#39; o&#39;,

&#39; &#39;,

&#39; d&#39;,

&#39; i&#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; c&#39;,

&#39; t&#39;,

&#39; e&#39;,

&#39; d&#39;,

&#39; &#39;,

&#39; b&#39;,

&#39; y&#39;,

&#39; &#39;,

&#39; t&#39;,

&#39; h&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; D&#39;,

&#39; i&#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; c&#39;,

&#39; t&#39;,

&#39; o&#39;,

&#39; r&#39;,

&#39; ‑&#39;,

&#39; G&#39;,

&#39; e&#39;,

&#39; n&#39;,

&#39; e&#39;,

&#39; r&#39;,

&#39; a&#39;,

&#39; l&#39;,

&#39; ,&#39;,

&#39; &#39;,

&#39; b&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; m&#39;,

&#39; o&#39;,

&#39; v&#39;,

&#39; e&#39;,

&#39; d&#39;,

&#39; &#39;,

&#39; f&#39;,

&#39; r&#39;,

&#39; o&#39;,

&#39; m&#39;,

&#39; &#39;,

&#39; t&#39;,

&#39; h&#39;,

&#39; a&#39;,

&#39; t&#39;,

&#39; &#39;,

&#39; w&#39;,

&#39; e&#39;,

&#39; l&#39;,

&#39; f&#39;,

&#39; a&#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; h&#39;,

&#39; o&#39;,

&#39; m&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; a&#39;,

&#39; n&#39;,

&#39; d&#39;,

&#39; &#39;,

&#39; a&#39;,

&#39; d&#39;,

&#39; m&#39;,

&#39; i&#39;,

&#39; t&#39;,

&#39; t&#39;,

&#39; e&#39;,

&#39; d&#39;,

&#39; &#39;,

&#39; t&#39;,

&#39; o&#39;,

&#39; &#39;,

&#39; a&#39;,

&#39; n&#39;,

&#39; o&#39;,

&#39; t&#39;,

&#39; h&#39;,

&#39; e&#39;,

&#39; r&#39;,

&#39; &#39;,

&#39; w&#39;,

&#39; e&#39;,

&#39; l&#39;,

&#39; f&#39;,

&#39; a&#39;,

&#39; r&#39;,

&#39; e&#39;,

&#39; &#39;,

&#39; h&#39;,

&#39; o&#39;,

&#39; m&#39;,

&#39; e&#39;,

&#39; .&#39;]
</code></pre>

<p>There is <em>no</em> difference between the two lines.</p>

<p>The problem was that when I passed a string of text into Differ, it compared based on the characters. You could see this effect in the documentation example:</p>

<pre><code> &#39;-   4. Complex is better than complicated.\n&#39;,
 &#39;?            ^                     ---- ^\n&#39;,
 &#39;+   4. Complicated is better than complex.\n&#39;,
 &#39;?           ++++ ^                      ^\n&#39;,
</code></pre>

<p>Instead of detecting that the whole word had changed, difflib noticed that the first four letters were alike.</p>

<p>A simple workaround is to provide Differ with lines instead of a single line of text. This was provided in the documentation by having the <code>\\n</code> (a newline character) as a line separator.</p>

<p>However, trying to break up a single line into several lines doesn’t solve the problem. Once an addition or subtraction to a phrase is detected, the difference propagates itself downstream, and soon the sentences are so different the algorithm gives up.</p>

<pre><code>[&#39;- No person shall import or export dutiable goods or tranship goods of a class dutiable &#39;,
 &#39;? ^^         ^^^^                                                              ---------\n&#39;,
 &#39;+ A person must not import or export dutiable goods or tranship goods of a class &#39;,
 &#39;? ^        ++ ^^^^^\n&#39;,
 &#39;- on import except under and in accordance with such regulations or restrictions as are prescribed. &#39;,
 &#39;?                                                                                       ------------\n&#39;,
 &#39;+ dutiable on import except under and in accordance with such regulations or restrictions as are &#39;,
 &#39;? +++++++++\n&#39;,
 &#39;+ prescribed. &#39;]
</code></pre>

<p>Once there is an addition or subtraction, the words are marked as changed when they are found in different parts, even though there was no change to the text.</p>

<p>This isn’t difflib’s fault. If difflib’s focus is on source code, then programmers are <em>discouraged</em> from writing long code in a single line. (In <a href="https://www.python.org/dev/peps/pep-0008/#maximum-line-length" rel="nofollow">PEP-8</a>, a style guide for Python, the maximum line length is 79 characters) Reading words in a giant block of characters is the norm for lawyers. Most legislation I have encountered so far stretches dozens of words over a single sentence 😓.</p>

<p>There’s also another problem anyway. Rightly or wrongly, lawyers don’t expect changes to be rendered this way. The most common method I have seen is using the most powerful LegalTech tool, Microsoft Word, to mark changes.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Untitled--2-.png" alt="Example of track changes in Microsoft Word."/></p>

<h2 id="introducing-redlines-a-simple-tweak-does-the-trick">Introducing Redlines — a simple tweak does the trick</h2>

<p>So, unfortunately, I needed to do some actual programming. How would I produce a redline comparison out of two texts? Gleaning some experience from <a href="mining-pdfs-to-obtain-better-text-from-decisions/" rel="nofollow">solving an earlier problem</a>, I decided to go back to the first principles. What would I do if I were a lawyer trying to redline a document?</p>

<p>It turned out to be quite simple — I would compare words and phrases, not characters.</p>

<p>So, the trick is to tokenise the text first. Tokens, for the most part, are individual words in a sentence. After tokenising the text, you have a sequence of tokens instead of characters. I tried to do this using a regular expression, which worked OK in my opinion.</p>

<pre><code class="language-python">import re

tokenizer = re.compile(r&#34;((?:[^()\s]+|[().?!-])\s*)&#34;)

def tokenize_text(text: str) -&gt; list[str]:
	# This function uses re to convert a string into tokens.
    return re.findall(tokenizer, text)
</code></pre>

<p>Once you have your sequences of tokens to compare, you can get difflibs to do the hard work for you.</p>

<p>Reading <a href="https://docs.python.org/3/library/difflib.html" rel="nofollow">the documentation and the source code of difflibs</a>, I found that it provided a low-level SequenceMatcher class. When sequences are compared using that class, it can provide<a href="https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.get_opcodes" rel="nofollow"> a list of changes needed to convert a source list of tokens to its result</a>. So, breaking that down, you can produce a redline representation instead of the original difflib representation.</p>

<pre><code class="language-python">    def output_markdown(self) -&gt; str:
        &#34;&#34;&#34;Returns the delta in markdown format.&#34;&#34;&#34;
        result = []
        style = &#39;red&#39;

        if self.options.get(&#39;markdown_style&#39;):
            style = self.options[&#39;markdown_style&#39;]

        if style == &#39;none&#39;:
            md_styles = {&#34;ins&#34;: (&#39;ins&#39;, &#39;ins&#39;), &#34;del&#34;: (&#39;del&#39;, &#39;del&#39;)}
        elif &#39;red&#39;:
            md_styles = {&#34;ins&#34;: (&#39;span style=&#34;color:red;font-weight:700;&#34;&#39;, &#39;span&#39;),
                         &#34;del&#34;: (&#39;span style=&#34;color:red;font-weight:700;text-decoration:line-through;&#34;&#39;, &#39;span&#39;)}

        for tag, i1, i2, j1, j2 in self.opcodes:
            if tag == &#39;equal&#39;:
                result.append(&#34;&#34;.join(self._seq1[i1:i2]))
            elif tag == &#39;insert&#39;:
                result.append(f&#34;&lt;{md_styles[&#39;ins&#39;][0]}&gt;{&#39;&#39;.join(self._seq2[j1:j2])}&lt;/{md_styles[&#39;ins&#39;][1]}&gt;&#34;)
            elif tag == &#39;delete&#39;:
                result.append(f&#34;&lt;{md_styles[&#39;del&#39;][0]}&gt;{&#39;&#39;.join(self._seq1[i1:i2])}&lt;/{md_styles[&#39;del&#39;][1]}&gt;&#34;)
            elif tag == &#39;replace&#39;:
                result.append(
                    f&#34;&lt;{md_styles[&#39;del&#39;][0]}&gt;{&#39;&#39;.join(self._seq1[i1:i2])}&lt;/{md_styles[&#39;del&#39;][1]}&gt;&#34;
                    f&#34;&lt;{md_styles[&#39;ins&#39;][0]}&gt;{&#39;&#39;.join(self._seq2[j1:j2])}&lt;/{md_styles[&#39;ins&#39;][1]}&gt;&#34;)

        return &#34;&#34;.join(result)
</code></pre>

<p>I liked the results — a simple markdown that can be rendered on any webpage.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Untitled--3-.png" alt="Output from my Jupyter Notebook showing a comparison between two sections of a law."/></p>

<p>I didn’t find anything on the Python Package Index that would have helped me produce a redline representation of text changes, so I <a href="https://pypi.org/project/redlines/" rel="nofollow">published mine anyway</a>. If you found it helpful, feel free to let me know!</p>

<h2 id="conclusion-batteries-included">Conclusion: Batteries Included</h2>

<p>I have always been mystified that Python is described as “<a href="https://www.python.org/dev/peps/pep-0206/#batteries-included-philosophy" rel="nofollow">batteries included</a>”. Indeed, in this particular case, I found that the standard module had a pretty good solution, and with some tweaks, I was able to make my own. I would never have been able to do this twenty years ago when there was hardly any “stack overflow”, open-source libraries or active developer communities. This also informs my views on what “No Code” should be — batteries included. It’s an exciting time to be involved in code and programming!</p>

<p><a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:redlines" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">redlines</span></a> <a href="https://write.as/houfu/tag:DataScience" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">DataScience</span></a> <a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:MicrosoftWord" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">MicrosoftWord</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/so-what-difference-does-it-make-drawing-red-lines-with-python" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/so-what-difference-does-it-make-drawing-red-lines-with-python</guid>
      <pubDate>Mon, 21 Feb 2022 09:02:00 +0000</pubDate>
    </item>
    <item>
      <title>How I Migrated My Docassemble Server From AWS to DigitalOcean</title>
      <link>https://write.as/houfu/how-i-migrated-my-docassemble-server-from-aws-to-digitalocean?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[I run a docassemble server at work, ostensibly introducing co-workers to a different way of using templates to generate their agreements. It&#39;s been pretty useful, so much so that I use it myself for my work. However, due to the pandemic, it&#39;s not been easy to go and sell it. Maybe I am going to have better luck soon.&#xA;&#xA;In the meantime, I decided to move the server from AWS to DigitalOcean.&#xA;&#xA;Why move?&#xA;&#xA;I liked the wide variety of features available on AWS, such as CodeCommit, Lambda and SES. DigitalOcean is not comparable in that regard. If I wanted to create a whole suite of services for my application, I would probably find something on AWS&#39;s glorious one-page filled with services.&#xA;&#xA;However, with great functions come great complexity. I had a headache trying to exploit them. I was not going to be able to make full use of their ecosystem. (I shall never scoff at AWS certification anymore.)&#xA;&#xA;On the other hand, I was more familiar with DigitalOcean and liked their straightforward pricing. So, if I wanted to move my pet project somewhere, I would have liked it to be in my backyard.&#xA;&#xA;Let&#39;s get moving!&#xA;&#xA;Lesson 1: Respect the shutdown&#xA;&#xA;The docassemble docs expressly ask you to shut down your docassemble server gracefully. This is not the usual docker stop container command but with a timeout flag. It isn&#39;t fatal to forget the timeout flag in many simple use cases, so you would never actually notice it.&#xA;&#xA;However, there&#39;s another way to kill your server in the cloud — flip the switch on your cloud instance on the management console. It doesn&#39;t feel like that when you click the red button, but it has the same effect. The cloud instance is sent straight to heaven, and there is nothing you can do about it.&#xA;&#xA;The shutdown is important because docassemble does quite a lot of work when it shuts down. It dumps the database records in your storage. If the storage is located in the cloud (like AWS&#39;s S3 or DigitalOcean&#39;s Spaces), there is some lag when sending all the files there. If the shutdown is not respected, the server&#39;s state is not saved, and you might not be able to restore it when you start the container.&#xA;&#xA;So with my AWS container gone in a cloud of dust, I found my files in my S3 storage were not updated. The last copy was over several months ago — the last time I had shut down my container normally. This meant that several months of work was gone! 😲&#xA;&#xA;Lesson 2: Restore from backup&#xA;&#xA;This blog could have ended on that sad note. Luckily for CloudOps newbies like me, docassemble automatically stores backups of the server state. These are stored in the backup folder of your storage and are arranged by date.&#xA;&#xA;If you, like me, borked your docassemble server and set it back to August 2020, you can grab your latest backup and replace the main directory files (outside backup). The process is described in the docassemble docs here. Instead of having no users back in August 2020, I managed to retrieve all my users in the Postgres database stored in the backups. Phew!&#xA;&#xA;Lesson 3: Check your config.yml file&#xA;&#xA;After this exercise, I decided to go with a DigitalOcean Droplet and AWS S3. Given that I was already on S3 and the costs of S3 are actually fairly negligible, this seems like a cost-effective combo. DigitalOcean spaces cost $5 no matter how big they are, whereas my S3 usage rarely comes up to more than a dollar.&#xA;&#xA;Before giving your new docassemble server a spin, do check your config.yml file. You can specify environment variables when you start a container, but once the server is running free, it uses the config.yml file found in the storage. If the configuration file was specially set for AWS, your server might not be able to run properly on DigitalOcean. This means you have to download the config.yml file on the storage (I used the web interface of S3 to do this) and edit it manually to fit your new server.&#xA;&#xA;In my setup, my original configuration file was set up for an AWS environment. This meant that my EC2 instance used security policies to access the S3. At the time, it simplified the set-up of the server. However, my Droplet cannot use these features. Generate an access key and secret key, and input these details and more in your updated config.yml file. Oh, and turn off ec2.&#xA;&#xA;If you are going to use Spaces, you will transfer the files in your old S3 to Spaces (I used s4cmd) and fill in the details of your S3 in the configuration file.&#xA;&#xA;Conclusion&#xA;&#xA;To be honest, the migration was essentially painless. The design of the docassemble server allows it to be restored from a single source of truth — the storage method you choose. Except for the problems that come from hand-editing your old config.yml (I had to type my SecretKey a few times 😢), you probably don&#39;t need to enter the docker and read initialize error logs. Given my positive experience, I will be well prepared to move back to AWS again! (Just kidding for now.)&#xA;&#xA;#tech #docassemble #AWS #DigitalOcean #docker #OpenSource #tutorial #CloudComputing&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38_ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/how-i-migrated-my-docassemble-server-from-aws-to-digitalocean&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>I run a docassemble server at work, ostensibly introducing co-workers to a different way of using templates to generate their agreements. It&#39;s been pretty useful, so much so that <em>I</em> use it myself for my work. However, due to the pandemic, it&#39;s not been easy to go and sell it. Maybe I am going to have better luck soon.</p>

<p>In the meantime, I decided to move the server from AWS to DigitalOcean.</p>

<h2 id="why-move">Why move?</h2>

<p>I liked the wide variety of features available on AWS, such as CodeCommit, Lambda and SES. DigitalOcean is not comparable in that regard. If I wanted to create a whole suite of services for my application, I would probably find <em>something</em> on AWS&#39;s glorious one-page filled with services.</p>

<p>However, with great functions come great complexity. I had a headache trying to exploit them. I was not going to be able to make full use of their ecosystem. (I shall never scoff at AWS certification anymore.)</p>

<p>On the other hand, I was more familiar with DigitalOcean and liked their straightforward pricing. So, if I wanted to move my pet project somewhere, I would have liked it to be in my backyard.</p>

<p>Let&#39;s get moving!</p>

<h2 id="lesson-1-respect-the-shutdown">Lesson 1: Respect the shutdown</h2>

<p>The docassemble docs expressly ask you to <a href="https://docassemble.org/docs/docker.html#shutdown" rel="nofollow">shut down your docassemble server gracefully</a>. This is not the usual <code>docker stop &lt;container&gt;</code> command but with a timeout flag. It isn&#39;t fatal to forget the timeout flag in many simple use cases, so you would never actually notice it.</p>

<p>However, there&#39;s another way to kill your server in the cloud — flip the switch on your cloud instance on the management console. It doesn&#39;t <em>feel</em> like that when you click the red button, but it has the same effect. The cloud instance is sent straight to heaven, and there is nothing you can do about it.</p>

<p>The shutdown is important because docassemble does quite a lot of work when it shuts down. It dumps the database records in your storage. If the storage is located in the cloud (like AWS&#39;s S3 or DigitalOcean&#39;s Spaces), there is some lag when sending all the files there. If the shutdown is not respected, the server&#39;s state is not saved, and you might not be able to restore it when you start the container.</p>

<p>So with my AWS container gone in a cloud of dust, I found my files in my S3 storage were not updated. The last copy was over several months ago — the last time I had shut down my container normally. This meant that several months of work was gone! 😲</p>

<h2 id="lesson-2-restore-from-backup">Lesson 2: Restore from backup</h2>

<p>This blog could have ended on that sad note. Luckily for CloudOps newbies like me, docassemble <a href="https://docassemble.org/docs/config.html#backup%20days" rel="nofollow">automatically stores backups of the server state</a>. These are stored in the <code>backup</code> folder of your storage and are arranged by date.</p>

<p>If you, like me, borked your docassemble server and set it back to August 2020, you can grab your latest backup and replace the main directory files (outside <code>backup</code>). The process is described in the docassemble <a href="https://docassemble.org/docs/docker.html#recovery" rel="nofollow">docs here</a>. Instead of having <em>no</em> users back in August 2020, I managed to retrieve all my users in the Postgres database stored in the backups. Phew!</p>

<h2 id="lesson-3-check-your-config-yml-file">Lesson 3: Check your config.yml file</h2>

<p>After this exercise, I decided to go with a DigitalOcean Droplet and AWS S3. Given that I was already on S3 and the costs of S3 are actually fairly negligible, this seems like a cost-effective combo. DigitalOcean spaces cost $5 no matter how big they are, whereas my S3 usage rarely comes up to more than a dollar.</p>

<p>Before giving your new docassemble server a spin, do check your <code>config.yml</code> file. You can specify environment variables when you start a container, but once the server is running free, it uses the <code>config.yml</code> file found in the storage. If the configuration file was specially set for AWS, your server might not be able to run properly on DigitalOcean. This means you have to download the <code>config.yml</code> file on the storage (I used the web interface of S3 to do this) and edit it manually to fit your new server.</p>

<p>In my setup, my original configuration file was set up for an AWS environment. This meant that my EC2 instance used security policies to access the S3. At the time, it simplified the set-up of the server. However, my Droplet cannot use these features. Generate an access key and secret key, and input these details and more in your updated <code>config.yml</code> file. Oh, and turn off <a href="https://docassemble.org/docs/config.html#ec2" rel="nofollow">ec2</a>.</p>

<p>If you are going to use Spaces, you will transfer the files in your old S3 to Spaces (I used <a href="https://github.com/bloomreach/s4cmd#s4cmd-cp-source-target" rel="nofollow">s4cmd</a>) and fill in the details of your S3 in the configuration file.</p>

<h2 id="conclusion">Conclusion</h2>

<p>To be honest, the migration was essentially painless. The design of the docassemble server allows it to be restored from a single source of truth — the storage method you choose. Except for the problems that come from hand-editing your old <code>config.yml</code> (I had to type my SecretKey a few times 😢), you probably don&#39;t <a href="https://docassemble.org/docs/docker.html#troubleshooting" rel="nofollow">need to enter the docker and read initialize error logs</a>. Given my positive experience, I will be well prepared to move back to AWS again! (Just kidding for now.)</p>

<p><a href="https://write.as/houfu/tag:tech" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tech</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:AWS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">AWS</span></a> <a href="https://write.as/houfu/tag:DigitalOcean" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">DigitalOcean</span></a> <a href="https://write.as/houfu/tag:docker" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docker</span></a> <a href="https://write.as/houfu/tag:OpenSource" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">OpenSource</span></a> <a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:CloudComputing" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">CloudComputing</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/how-i-migrated-my-docassemble-server-from-aws-to-digitalocean" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/how-i-migrated-my-docassemble-server-from-aws-to-digitalocean</guid>
      <pubDate>Mon, 29 Mar 2021 05:54:00 +0000</pubDate>
    </item>
    <item>
      <title>Using Docassemble with Traefik and HTTPS</title>
      <link>https://write.as/houfu/using-docassemble-with-traefik-and-https?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;Things you can only do during a lockdown – install a new server. As I mentioned previously, I got round to installing the latest Ubuntu LTS on my home server. Instead of using apt get for new software, I would run my server services through Docker instead. First up, I got Pi-Hole working and blocking ads. I&#39;s been sweet.&#xA;&#xA;Let’s Play with: Pi-HoleI try to install Pi-Hole Server to block all ads and tracking websites at home.Love.Law.Robots.Houfu&#xA;&#xA;My conviction to use containers started with docassemble. You can use docassemble to generate contracts from answering questions. It&#39;s relevant to my work and I am trying to get more of my (non-legal) colleagues to try it. Unlike other software I have tried, docassemble recommends just using docker. With one command, docker run -d jhpyle/docassemble, I would get a fully-featured server. My mind was blown.&#xA;&#xA;DocassembleA free, open-source expert system for guided interviews and document assembly, based on Python, YAML, and Markdown.Docassemble&#xA;&#xA;However, as I became more familiar with how to get docker to do what I want, the limitations of that simple command began to restrict me. Docassemble uses several ports. Many other applications share the same port, especially for a web server: 80 and 443. If docker and docassemble took these ports, no one else was going to get them. I wasn&#39;t sure if I wanted my home server to be just a docassemble server.&#xA;&#xA;Furthermore, using secure ports (HTTPS) became a serious problem. I wanted to use my home server&#39;s docassemble installation as a development base, so it should be accessible to the outside world. For some reason, docassemble wouldn&#39;t accept my wildcard certs. If I planned to use it for anything serious, having an unsecured website was impossible.  &#xA;  &#xA;It got so frustrating that I gave up.&#xA;&#xA;Enter the Reverse-Proxy: Traefik&#xA;&#xA;The short answer to my problems was to use a reverse proxy. A reverse proxy is a kind of server that gets information from another server for a client. Or in this case, a traefik server receives a request and figures out which docker container it should go to. A traefik server can also do other things, such as providing end to end security for your communications by obtaining free SSL certificates from Let&#39;s Encrypt.&#xA;&#xA;TraefikTraefik Documentationlogo&#xA;&#xA;I was convinced to go for this because it claimed that it would make &#34;publishing your services a fun and easy experience&#34;. When I read that, I let a tear go. Is it actually possible for this program to automatically detect the right configuration for your services? Even for something as big as docassemble?  &#xA;  &#xA;I&#39;ll let you be the judge of that at the end of this article.&#xA;&#xA;Step 1: Set up Traefik&#xA;&#xA;Of course, you would need to have docker set up and good to go.  &#xA;  &#xA;There are a bunch of ways to get Traefik going, but I would be using a docker-compose.yml file like in the QuickStart.   &#xA;  &#xA;The documentation for docassemble does not mention anything about docker compose. It is a shame because I found it to be a more user-friendly tool than the docker command-line interface. So instead of writing a bash script just to shorten my long docker run command, I would write out the blueprint of my setup in the docker-compose.yml. After that, I can run docker-compose up -d and the services in the file will start altogether.  &#xA;  &#xA;This is very important in my setup, because there are other services in my home server like plex or grocy (another lockdown project) too. For the sake of convenience, I decided to include all these like projects in the same docker-compose.yml file. This is the blueprint of my home server!  &#xA;  &#xA;Back to Traefik, this is the section of my docker-compose.yml file setting out the reverse proxy server:&#xA;    &#xA;    &#xA;    services:&#xA;      reverse-proxy:&#xA;        # The official v2 Traefik docker image&#xA;        image: traefik:v2.2&#xA;        containername: traefik&#xA;        # Enables the web UI and tells Traefik to listen to docker&#xA;        command: --api.insecure=true --providers.docker&#xA;        ports:&#xA;          # The HTTP/HTTPS port&#xA;          &#34;80:80&#34;&#xA;          &#34;443:443&#34;&#xA;          # The Web UI (enabled by --api.insecure=true)&#xA;          &#34;8080:8080&#34;&#xA;        volumes:&#xA;          # So that Traefik can listen to the Docker events&#xA;          /var/run/docker.sock:/var/run/docker.sock&#xA;          /home/houfu/traefik/:/etc/traefik/&#xA;        environment:&#xA;          DOAUTHTOKEN: XXX&#xA;        restart: unless-stopped&#xA;&#xA;Just a few notes:&#xA;&#xA;  This line /home/houfu/traefik/:/etc/traefik/ under volumes allows me to have access to the configuration file used by traefik.&#xA;  This line DOAUTHTOKEN: XXX under environment is to generate SSL certificates using my personal domain, which is managed by DigitalOcean.&#xA;&#xA;Step 2: Prepare Traefik to generate SSL Certificates&#xA;&#xA;Instead of having docassemble obtain the SSL certificates to use HTTPS, I decided to get Traefik to do it instead. Reverse proxies do this job much better, and I wouldn&#39;t need to &#34;enter&#34; the docassemble container to hunt down why SSL is not working. &#xA;&#xA;Besides, my other services on my home server were already getting their certificates through Traefik, so getting docassemble to do the same would be straightforward right?&#xA;&#xA;For this step, you would need to define a certificate resolver for Traefik to use. Please read the documentation as it is quite informative. For my set-up, I decided to use DigitalOcean as I was already using it for my DNS.&#xA;&#xA;In the configuration file (traefik.toml), add a section to define the certificate resolver.&#xA;    &#xA;    &#xA;    [certificatesResolvers.docassembleResolver.acme]&#xA;        email = &#34;admin@admin.com&#34;&#xA;        storage = &#34;acme.json&#34;&#xA;    &#xA;    [certificatesResolvers.docassembleResolver.acme.dnsChallenge]&#xA;        # used during the challenge&#xA;        provider = &#34;digitalocean&#34;&#xA;&#xA;The final step, especially if you have chosen DigitalOcean as a provider, is to get an API key and provide it to Traefik so that the process of getting a certificate can be automated. This was the DOAUTHTOKEN in the docker-compose.yml file referred to in the first step.&#xA;&#xA;Step 3: Provide a blueprint for the Docassemble service&#xA;&#xA;Once we have the reverse proxy set up, it’s time to get docassemble to run. This is the final form of the docker-compose.yml file for the docassemble service.&#xA;    &#xA;    &#xA;    docassemble:&#xA;        image: &#34;jhpyle/docassemble:latest&#34;&#xA;        hostname: docassemble&#xA;        containername: docassemble&#xA;        stopgraceperiod: 1m30s&#xA;        environment:&#xA;          CONTAINERROLE=all&#xA;          DBPREFIX=postgresql+psycopg2://&#xA;          DBNAME=docassemble&#xA;          DBUSER=docassemble&#xA;          DBPASSWORD=abc123&#xA;          DBHOST=localhost&#xA;          USEHTTPS=false&#xA;          DAHOSTNAME=docassemble.example.com&#xA;          USELETSENCRYPT=false&#xA;          S3ENABLE=true&#xA;          S3ACCESSKEY=ABCDEFGH&#xA;          S3SECRETACCESSKEY=1234567&#xA;          S3BUCKET=docassemble&#xA;          S3ENDPOINTURL=https://xxxx.sgp1.digitaloceanspaces.com&#xA;          TIMEZONE=Asia/Singapore&#xA;          DAPYTHONVERSION=3&#xA;        labels:&#xA;          traefik.backend=docassemble&#xA;          traefik.http.routers.docassemble.rule=Host(docassemble.example.com)&#xA;          traefik.http.services.docassemble.loadbalancer.server.port=80&#xA;          traefik.http.routers.docassemble.tls=true&#xA;          traefik.http.routers.docassemble.tls.certresolver=docassembleResolver&#xA;          traefik.http.middlewares.docassemble-redirect.redirectscheme.scheme=https&#xA;          traefik.http.middlewares.docassemble-redirect.redirectscheme.permanent=true&#xA;          traefik.http.routers.docassemble.middlewares=docassemble-redirect&#xA;&#xA;One of the most important aspects of setting up your own docassemble server is figuring out the environment variables. The docassemble documentation recommends that we use an env.list file or pass a list of configuration values to the docker run command. For our docker-compose file, we pass them as a dictionary to the environment section of the service blueprint. Feel free to add or modify these options as you need. For example, you can see that I am using DigitalOcean Spaces as my S3 compatible storage.&#xA;&#xA;So where does the magic of Trafik’s automatic configuration come in? Innocuously under the label section of the blueprint. Let’s split this up for easy explanation.&#xA;    &#xA;    &#xA;    labels:&#xA;      traefik.backend=docassemble&#xA;      traefik.http.routers.docassemble.rule=Host(docassemble.example.com)&#xA;      traefik.http.services.docassemble.loadbalancer.server.port=80&#xA;    &#xA;&#xA;In the first block of labels, we define the name and the host of the docassemble server. Traefik now knows what to call this server, and to direct queries from “docassemble.example.com” to this server. As docassemble exposes several ports, we also help prod traefik to use the correct port to access the server.&#xA;    &#xA;    &#xA;    labels:&#xA;      traefik.http.routers.docassemble.tls=true&#xA;      traefik.http.routers.docassemble.tls.certresolver=docassembleResolver&#xA;    &#xA;&#xA;In this block of labels, we tell Traefik to use HTTPS and to use the certificate provider we defined earlier to get these certificates.&#xA;    &#xA;    &#xA;    labels:&#xA;      traefik.http.middlewares.docassemble-redirect.redirectscheme.scheme=https&#xA;      traefik.http.middlewares.docassemble-redirect.redirectscheme.permanent=true&#xA;      traefik.http.routers.docassemble.middlewares=docassemble-redirect&#xA;    &#xA;&#xA;Finally we tell traefik to use a middleware here — a redirect. The redirect middleware ensures that uses will use HTTPS to communicate with the server.&#xA;&#xA;Note that in our environment variables for the docassemble server, we tell docassemble not to use https (“USEHTTPS=false”). This is because traefik is already taking care of it. We don’t need docassemble to bother with it.&#xA;&#xA;It works!&#xA;&#xA;Docassemble servers take a bit of time to set up. But once you get it up, you will see my favourite screen in the entire application.&#xA;&#xA;docassemble server is working.I would like to thank my...&#xA;&#xA;Notice the grey padlock in the address bar of my Firefox browser? That’s right, HTTPS baby!!&#xA;&#xA;Final Thoughts&#xA;&#xA;I am glad I learnt a lot about docker from docassemble, and its documentation is top-notch for what it is. However, running one is not easy. Using docker-compose helped iron out some of the pain. In any case, I am glad I got over this. It’s time to get developing! What should I work on next?&#xA;&#xA;#blog #docassemble #docker #tutorial #tech #Traefik #HTTPS&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/using-docassemble-with-traefik-and-https&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/sven-schlager-xzjouTJASSA-unsplash.jpg" alt="Feature image"/></p>

<p>Things you can only do during a lockdown – install a new server. As I mentioned previously, I got round to installing the latest Ubuntu LTS on my home server. Instead of using <code>apt get</code> for new software, I would run my server services through Docker instead. First up, I got Pi-Hole working and blocking ads. I&#39;s been sweet.</p>

<p><a href="https://www.lovelawrobots.com/lets-play-with-pi-hole/" rel="nofollow">Let’s Play with: Pi-HoleI try to install Pi-Hole Server to block all ads and tracking websites at home.<img src="https://www.lovelawrobots.com/favicon.png" alt=""/>Love.Law.Robots.Houfu<img src="https://www.lovelawrobots.com/content/images/2021/05/plum-cake-3641849_1280.jpg" alt=""/></a></p>

<p>My conviction to use containers started with <a href="https://docassemble.org" rel="nofollow">docassemble</a>. You can use docassemble to generate contracts from answering questions. It&#39;s relevant to my work and I am trying to get more of my (non-legal) colleagues to try it. Unlike other software I have tried, docassemble recommends just using docker. With one command, <code>docker run -d jhpyle/docassemble</code>, I would get a fully-featured server. My mind was blown.</p>

<p><a href="https://docassemble.org" rel="nofollow">DocassembleA free, open-source expert system for guided interviews and document assembly, based on Python, YAML, and Markdown.Docassemble<img src="http://docassemble.org/img/docassemble-logo.jpg" alt=""/></a></p>

<p>However, as I became more familiar with how to get docker to do what <em>I</em> want, the limitations of that simple command began to restrict me. Docassemble uses several ports. Many other applications share the same port, especially for a web server: 80 and 443. If docker and docassemble took these ports, no one else was going to get them. I wasn&#39;t sure if I wanted my home server to be just a docassemble server.</p>

<p>Furthermore, using secure ports (HTTPS) became a serious problem. I wanted to use my home server&#39;s docassemble installation as a development base, so it should be accessible to the outside world. For some reason, docassemble wouldn&#39;t accept my wildcard certs. If I planned to use it for anything serious, having an unsecured website was impossible.</p>

<p>It got so frustrating that I gave up.</p>

<h2 id="enter-the-reverse-proxy-traefik">Enter the Reverse-Proxy: Traefik</h2>

<p>The short answer to my problems was to use a reverse proxy. A reverse proxy is a kind of server that gets information from another server for a client. Or in this case, a <a href="https://docs.traefik.io/" rel="nofollow">traefik</a> server receives a request and figures out which docker container it should go to. A traefik server can also do other things, such as providing end to end security for your communications by obtaining free SSL certificates from Let&#39;s Encrypt.</p>

<p><a href="https://docs.traefik.io/" rel="nofollow">TraefikTraefik Documentation<img src="https://docs.traefik.io/assets/img/traefikproxy-icon-color.png" alt=""/>logo<img src="https://docs.traefik.io/assets/img/traefik-architecture.png" alt=""/></a></p>

<p>I was convinced to go for this because it claimed that it would make “publishing your services a fun and easy experience”. When I read that, I let a tear go. Is it actually possible for this program to <em>automatically</em> detect the right configuration for your services? Even for something as big as docassemble?</p>

<p>I&#39;ll let you be the judge of that at the end of this article.</p>

<h2 id="step-1-set-up-traefik">Step 1: Set up Traefik</h2>

<p>Of course, you would need to have <a href="https://www.docker.com/get-started" rel="nofollow">docker</a> set up and good to go.</p>

<p>There are a bunch of ways to get Traefik going, but I would be using a <code>docker-compose.yml</code> file like in the <a href="https://docs.traefik.io/getting-started/quick-start/" rel="nofollow">QuickStart</a>.</p>

<p>The documentation for docassemble does not mention anything about <code>docker compose.</code> It is a shame because I found it to be a more user-friendly tool than the <code>docker</code> command-line interface. So instead of writing a bash script just to shorten my long <code>docker run</code> command, I would write out the blueprint of my setup in the <code>docker-compose.yml</code>. After that, I can run <code>docker-compose up -d</code> and the services in the file will start altogether.</p>

<p>This is very important in my setup, because there are other services in my home server like plex or grocy (another lockdown project) too. For the sake of convenience, I decided to include all these like projects in the same <code>docker-compose.yml</code> file. This is <em>the</em> blueprint of my home server!</p>

<p>Back to Traefik, this is the section of my <code>docker-compose.yml</code> file setting out the reverse proxy server:</p>

<p>    services:
      reverse-proxy:
        # The official v2 Traefik docker image
        image: traefik:v2.2
        container<em>name: traefik
        # Enables the web UI and tells Traefik to listen to docker
        command: —api.insecure=true —providers.docker
        ports:
          # The HTTP/HTTPS port
          – “80:80”
          – “443:443”
          # The Web UI (enabled by —api.insecure=true)
          – “8080:8080”
        volumes:
          # So that Traefik can listen to the Docker events
          – /var/run/docker.sock:/var/run/docker.sock
          – /home/houfu/traefik/:/etc/traefik/
        environment:
          DO</em>AUTH_TOKEN: XXX
        restart: unless-stopped</p>

<p>Just a few notes:</p>
<ul><li>This line <code>/home/houfu/traefik/:/etc/traefik/</code> under <code>volumes</code> allows me to have access to the configuration file used by traefik.</li>
<li>This line <code>DO_AUTH_TOKEN: XXX</code> under <code>environment</code> is to generate SSL certificates using my personal domain, which is managed by DigitalOcean.</li></ul>

<h2 id="step-2-prepare-traefik-to-generate-ssl-certificates">Step 2: Prepare Traefik to generate SSL Certificates</h2>

<p>Instead of having docassemble obtain the SSL certificates to use HTTPS, I decided to get Traefik to do it instead. Reverse proxies do this job much better, and I wouldn&#39;t need to “enter” the docassemble container to hunt down why SSL is not working.</p>

<p>Besides, my other services on my home server were already getting their certificates through Traefik, so getting docassemble to do the same would be straightforward right?</p>

<p>For this step, you would need to define a certificate resolver for Traefik to use. Please read <a href="https://docs.traefik.io/https/acme/" rel="nofollow">the documentation</a> as it is quite informative. For my set-up, I decided to use DigitalOcean as I was already using it for my DNS.</p>

<p>In the configuration file (<code>traefik.toml</code>), add a section to define the certificate resolver.</p>

<p>    [certificatesResolvers.docassembleResolver.acme]
        email = “admin@admin.com”
        storage = “acme.json”</p>

<p>    [certificatesResolvers.docassembleResolver.acme.dnsChallenge]
        # used during the challenge
        provider = “digitalocean”</p>

<p>The final step, especially if you have chosen DigitalOcean as a provider, is to get an API key and provide it to Traefik so that the process of getting a certificate can be automated. This was the <code>DO_AUTH_TOKEN</code> in the <code>docker-compose.yml</code> file referred to in the first step.</p>

<h2 id="step-3-provide-a-blueprint-for-the-docassemble-service">Step 3: Provide a blueprint for the Docassemble service</h2>

<p>Once we have the reverse proxy set up, it’s time to get docassemble to run. This is the final form of the <code>docker-compose.yml</code> file for the docassemble service.</p>

<p>    docassemble:
        image: “jhpyle/docassemble:latest”
        hostname: docassemble
        container<em>name: docassemble
        stop</em>grace_period: 1m30s
        environment:
          – CONTAINERROLE=all
          – DBPREFIX=postgresql+psycopg2://
          – DBNAME=docassemble
          – DBUSER=docassemble
          – DBPASSWORD=abc123
          – DBHOST=localhost
          – USEHTTPS=false
          – DAHOSTNAME=docassemble.example.com
          – USELETSENCRYPT=false
          – S3ENABLE=true
          – S3ACCESSKEY=ABCDEFGH
          – S3SECRETACCESSKEY=1234567
          – S3BUCKET=docassemble
          – S3ENDPOINTURL=<a href="https://xxxx.sgp1.digitaloceanspaces.com" rel="nofollow">https://xxxx.sgp1.digitaloceanspaces.com</a>
          – TIMEZONE=Asia/Singapore
          – DAPYTHONVERSION=3
        labels:
          – traefik.backend=docassemble
          – traefik.http.routers.docassemble.rule=Host(<code>docassemble.example.com</code>)
          – traefik.http.services.docassemble.loadbalancer.server.port=80
          – traefik.http.routers.docassemble.tls=true
          – traefik.http.routers.docassemble.tls.certresolver=docassembleResolver
          – traefik.http.middlewares.docassemble-redirect.redirectscheme.scheme=https
          – traefik.http.middlewares.docassemble-redirect.redirectscheme.permanent=true
          – traefik.http.routers.docassemble.middlewares=docassemble-redirect</p>

<p>One of the most important aspects of setting up your own docassemble server is figuring out the environment variables. The <a href="https://docassemble.org/docs/docker.html#configuration%20options" rel="nofollow">docassemble documentation recommends</a> that we use an <code>env.list</code> file or pass a list of configuration values to the <code>docker run</code> command. For our docker-compose file, we pass them as a dictionary to the <code>environment</code> section of the service blueprint. Feel free to add or modify these options as you need. For example, you can see that I am using <a href="https://www.digitalocean.com/products/spaces/" rel="nofollow">DigitalOcean Spaces</a> as my S3 compatible storage.</p>

<p>So where does the magic of Trafik’s automatic configuration come in? Innocuously under the <code>label</code> section of the blueprint. Let’s split this up for easy explanation.</p>

<p>    labels:
      – traefik.backend=docassemble
      – traefik.http.routers.docassemble.rule=Host(<code>docassemble.example.com</code>)
      – traefik.http.services.docassemble.loadbalancer.server.port=80</p>

<p>In the first block of labels, we define the name and the host of the docassemble server. Traefik now knows what to call this server, and to direct queries from “docassemble.example.com” to this server. As docassemble exposes several ports, we also help prod traefik to use the correct port to access the server.</p>

<p>    labels:
      – traefik.http.routers.docassemble.tls=true
      – traefik.http.routers.docassemble.tls.certresolver=docassembleResolver</p>

<p>In this block of labels, we tell Traefik to use HTTPS and to use the certificate provider we defined earlier to get these certificates.</p>

<p>    labels:
      – traefik.http.middlewares.docassemble-redirect.redirectscheme.scheme=https
      – traefik.http.middlewares.docassemble-redirect.redirectscheme.permanent=true
      – traefik.http.routers.docassemble.middlewares=docassemble-redirect</p>

<p>Finally we tell traefik to use a middleware here — a redirect. The redirect middleware ensures that uses will use HTTPS to communicate with the server.</p>

<p>Note that in our environment variables for the docassemble server, we tell docassemble <strong>not to use https</strong> (“USEHTTPS=false”). This is because traefik is already taking care of it. We don’t need docassemble to bother with it.</p>

<h2 id="it-works">It works!</h2>

<p>Docassemble servers take a bit of time to set up. But once you get it up, you will see my favourite screen in the entire application.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Screenshot-2020-05-15-at-1.22.28-AM.png" alt="docassemble server is working."/>I would like to thank my...</p>

<p>Notice the grey padlock in the address bar of my Firefox browser? That’s right, HTTPS baby!!</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>I am glad I learnt a lot about docker from docassemble, and its documentation is top-notch for what it is. However, running one is not easy. Using docker-compose helped iron out some of the pain. In any case, I am glad I got over this. It’s time to get developing! What should I work on next?</p>

<p><a href="https://write.as/houfu/tag:blog" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">blog</span></a> <a href="https://write.as/houfu/tag:docassemble" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docassemble</span></a> <a href="https://write.as/houfu/tag:docker" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">docker</span></a> <a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:tech" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tech</span></a> <a href="https://write.as/houfu/tag:Traefik" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Traefik</span></a> <a href="https://write.as/houfu/tag:HTTPS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">HTTPS</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/using-docassemble-with-traefik-and-https" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/using-docassemble-with-traefik-and-https</guid>
      <pubDate>Thu, 14 May 2020 14:06:00 +0000</pubDate>
    </item>
    <item>
      <title>Automate Boring Stuff: Get Python and your Web Browser to download your judgements</title>
      <link>https://write.as/houfu/automate-boring-stuff-get-python-and-your-web-browser-to-download-your?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Feature image&#xA;&#xA;  This post is part of a series on my Data Science journey with PDPC Decisions. Check it out for more posts on visualisations, natural languge processing, data extraction and processing!&#xA;&#xA; Update 13 June 2020: “At least until the PDPC breaks its website.” How prescient… about three months after I wrote this post, the structure of the PDPC’s website was drastically altered. The concepts and the ideas in this post haven’t changed, but the examples are outdated. This gives me a chance to rewrite this post. If I ever get round to it, I’ll provide a link.&#xA;&#xA;Regular readers would already know that I try to run a github repository which tries to compile all personal data protection decisions in Singapore. Decisions are useful resources teeming with lots of information. They have statistics, insights into what factors are relevant in decision making and show that data protection is effective in Singapore. Even basic statistics about decisions make newspaper stories here locally. It would be great if there was a way to mine all that information!&#xA;&#xA;houfu/pdpc-decisionsData Protection Enforcement Cases in Singapore. Contribute to houfu/pdpc-decisions development by creating an account on GitHub.GitHubhoufu&#xA;&#xA;Unfortunately, using the Personal Data Protection Commission in Singapore’s website to download judgements can be painful.&#xA;&#xA;This is our target webpage today - Note the website has been transformed.&#xA;&#xA;As you can see, you are only able to view no more than 5 decisions at one time. As the first decision dates back to 2016, you will have to go through several pages to grab everything! Actually just 23. I am sure you can do all that in 1 night, right? Right?&#xA;&#xA;If you are not inclined to do it, then get your computer to do it. Using selenium, I wrote a python script to automate the whole process of finding all the decisions available on the website. What could have been a tedious night’s work was accomplished in 34 seconds.&#xA;&#xA;Check out the script here.&#xA;&#xA;What follows here is a step by step write up of how I did it. So hang on tight!&#xA;&#xA;Section 1: Observe your quarry&#xA;&#xA;Before setting your computer loose on a web page, it pays to understand the structure and inner workings of your web page. Open this up by using your favourite browser. For Chrome, this is Developer&#39;s Tools and in Firefox, this is Web Developer. You will be looking for a tab called Sources, which shows you the HTML code of the web page.&#xA;&#xA;Play with the structure of the web page by hovering over various elements of the web page with your mouse. You can then look for the exact elements you need to perform your task:&#xA;&#xA;  In order to see a new page, you will have to click on the page number in the pagination. This is under a section (a CSS class) called group_pages. Each page-number is under a section (another CSS class) called page-number.&#xA;  Each decision has its own section (a CSS class) named press-item. The link to the download, which is either to a text file or a PDF file, is located in a link in each press-item.&#xA;  Notice too that each press-item also has other metadata regarding the decision. For now, we are curious about the date of the decision and the respondent.&#xA;&#xA;Section 2: Decide on a strategy&#xA;&#xA;Having figured out the website, you can decide on how to achieve your goal. In this case, it would be pretty similar to what you would have done manually.&#xA;&#xA;  Start on a page&#xA;  Click on a link to download&#xA;  Go to the next link until there are no more links&#xA;  Move on to the next page&#xA;  Keep repeating steps 1 to 4 until there are no more pages&#xA;  Profit!&#xA;&#xA;Since we did notice the metadata, let’s use it. If you don’t use what is already in front of you, you will have to read the decision to extract such information In fact, we are going to use the metadata to name our decision.&#xA;&#xA;Section 3: Get your selenium on it!&#xA;&#xA;Selenium drives a web browser. It mimics user interactions on the web browser, so our strategy in Step 2 is straightforward to implement. Instead of moving our mouse like we ordinarily would, we would tell the web driver what to do instead.&#xA;&#xA;WebDriver :: Documentation for SeleniumDocumentation for SeleniumSelenium&#xA;&#xA;Let’s translate our strategy to actual code.&#xA;&#xA;Step 1: Start on a page&#xA;&#xA;We are going to need to start our web driver and get it to run on our web page.&#xA;    &#xA;    &#xA;    from selenium.webdriver import Chrome&#xA;    from selenium.webdriver.chrome.options import Options&#xA;    PDPCdecisionssite = &#34;https://www.pdpc.gov.sg/Commissions-Decisions/Data-Protection-Enforcement-Cases&#34;&#xA;    # Setup webdriver&#xA;    options = Options()&#xA;    # Uncomment the next two lines for a headless chrome&#xA;    # options.addargument(&#39;--headless&#39;)&#xA;    # options.addargument(&#39;--disable-gpu&#39;)&#xA;    # options.addargument(&#39;--window-size=1920,1080&#39;)&#xA;    driver = Chrome(options=options)&#xA;    driver.get(PDPCdecisionssite)&#xA;    &#xA;&#xA;Steps 2: Download the file&#xA;&#xA;Now that you have prepared your page, let’s drill down to the individual decisions itself. As we figured out earlier, each decision is found in a section named press-item. Get selenium to collect all the decisions on the page.&#xA;    &#xA;    &#xA;    judgements = driver.findelementsbyclassname(&#39;press-item&#39;)&#xA;    &#xA;&#xA;Recall that we were not just going to download the file, we will also be using the date of the decision and the respondent to name the file. For the date function, I found out that under each press-item there is a press-date which gives us the text of the decision date; we can easily convert this to a python datetime so we can format it anyway we like.&#xA;    &#xA;    &#xA;    def getdate(item: WebElement):&#xA;        itemdate = datetime.strptime(item.findelementbyclassname(&#39;press_date&#39;).text, &#34;%d %b %Y&#34;)&#xA;        return itemdate.strftime(&#34;%Y-%m-%d&#34;)&#xA;    &#xA;&#xA;For the respondent, the heading (which is written in a fixed format and also happens to be the link to the download – score!) already gives you the information. Use a regular expression on the text of the link to suss it out. (One of the decisions do not follow the format of “Breach … by respondent “, so the alternative is also laid out)&#xA;    &#xA;    &#xA;    def getrespondent(item):&#xA;        text = item.text&#xA;        return re.split(r&#34;\s+[bB]y|[Aa]gainst\s+&#34;, text, re.I)[1]&#xA;    &#xA;&#xA;You are now ready to download a file! Using the metadata and the link you just found, you can come up with meaningful names to download your files. Naming your own files will also help you avoid the idiosyncratic ways the PDPC names its own downloads.&#xA;&#xA;Note that some of the files are not PDF downloads but instead are short texts in web pages. Using the earlier strategies, you can figure out what information you need. This time, I used BeautifulSoup to get the information. I did not want to use selenium to do any unnecessary navigation. Treat PDFs and web pages differently.&#xA;    &#xA;    &#xA;    def downloadfile(item, filedate, filerespondent):&#xA;        url = item.getproperty(&#39;href&#39;)&#xA;        print(&#34;Downloading a File: &#34;, url)&#xA;        print(&#34;Date of Decision: &#34;, filedate)&#xA;        print(&#34;Respondent: &#34;, filerespondent)&#xA;        if url[-3:] == &#39;pdf&#39;:&#xA;            dest = SOURCEFILEPATH + filedate + &#39; &#39; + filerespondent + &#39;.pdf&#39;&#xA;            wget.download(url, out=dest)&#xA;        else:&#xA;            with open(SOURCEFILEPATH + filedate + &#39; &#39; + filerespondent + &#39;.txt&#39;, &#34;w&#34;) as f:&#xA;                from bs4 import BeautifulSoup&#xA;                from urllib.request import urlopen&#xA;                soup = BeautifulSoup(urlopen(url), &#39;html5lib&#39;)&#xA;                text = soup.find(&#39;div&#39;, class=&#39;rte&#39;).getText()&#xA;                lines = re.split(r&#34;ns+&#34;, text)&#xA;                f.writelines([line + &#39;n&#39; for line in lines if line != &#34;&#34;])&#xA;    &#xA;&#xA;Steps 3 to 5: Download every item on every page&#xA;&#xA;The next steps follow a simple idiom — for every page and for every item on each page, download a file.&#xA;    &#xA;    &#xA;    for pagecount in range(len(pages)):&#xA;            pages[pagecount].click()&#xA;            print(&#34;Now at Page &#34;, pagecount)&#xA;            pages = refreshpages(driver)&#xA;            judgements = driver.findelementsbyclassname(&#39;press-item&#39;)&#xA;            for judgement in judgements:&#xA;                date = getdate(judgement)&#xA;                link = judgement.findelementbytagname(&#39;a&#39;)&#xA;                respondent = getrespondent(link)&#xA;                downloadfile(link, date, respondent)&#xA;    &#xA;&#xA;Unfortunately, once selenium changes a page, it needs to be refreshed. We are going to need a new grouppages and page-number in order to continue accessing the page. I wrote a function to “refresh” the variables I am using to access these sections.&#xA;    &#xA;    &#xA;    def refreshpages(webdriver: Chrome):&#xA;        grouppages = webdriver.findelementbyclassname(&#39;grouppages&#39;)&#xA;        return grouppages.findelementsbyclassname(&#39;page-number&#39;)&#xA;    . . .&#xA;    pages = refreshpages(driver)&#xA;    &#xA;&#xA;Conclusion&#xA;&#xA;Once you got your web driver to be thorough, you are done! In my last pass, 115 decisions were downloaded in 34 seconds. The best part is that you can repeat this any time there are new decisions. Data acquisition made easy! At least until the PDPC breaks its website.&#xA;&#xA;Postscript: Is this… Illegal?&#xA;&#xA;I’m listening…&#xA;&#xA;Web scraping has always been quite controversial and the stakes can be quite high. Copyright infringement, Misuse of Computer Act and trespass, to name a few. Funnily enough, manually downloading may be less illegal than using a computer. The PDPC’s own terms of use is not on point at this. &#xA;&#xA;( Update 15 Mar 2021 : OK I think I am being fairly obtuse about this. There is a paragraph that states you can’t use robots or spiders to monitor their website. That might make sense in the past when data transfers were expensive, but I don&#39;t think that this kind of activity at my scale can crash a server.) &#xA;&#xA;Personally, I feel this particular activity is fairly harmless — this is considered “own personal, non-commercial use” to me. I would likely continue with this for as long as I would like my own collection of decisions. Or until they provide better programmatic access to their resources.&#xA;&#xA;Ready to mine free online legal materials in Singapore? Not so fast!Amendments to Copyright Act might support better access to free online legal materials in Singapore by robots. I survey government websites to find out how friendly they are to this.Love.Law.Robots.HoufuIn 2021, the Copyright Act in Singapore was amended to support data analysis, like web scraping? I wrote this follow-up post.&#xA;&#xA;#PDPC-Decisions #Programming #Python #tutorial #Updated&#xA;&#xA;img src=&#34;https://res.cloudinary.com/lovelawrobots/image/upload/fauto,qauto/v1/blog-images/photo2023-06-3023-47-38ozpz52&#34; alt=&#34;Author Portrait&#34; width=&#34;320&#34;/&#xD;&#xA;Love.Law.Robots. - A blog by Ang Hou Fu&#xD;&#xA;&#xD;&#xA;a href=&#34;https://remark.as/p/houfu/automate-boring-stuff-get-python-and-your-web-browser-to-download-your&#34;Discuss.../a this Post&#xD;&#xA;If you found this post useful, or like my work, a tip is always appreciated:  script type=&#34;text/javascript&#34; src=&#34;https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js&#34; data-name=&#34;bmc-button&#34; data-slug=&#34;damonhoufbq&#34; data-color=&#34;#BD5FFF&#34; data-emoji=&#34;&#34;  data-font=&#34;Poppins&#34; data-text=&#34;Buy me a coffee&#34; data-outline-color=&#34;#000000&#34; data-font-color=&#34;#ffffff&#34; data-coffee-color=&#34;#FFDD00&#34; /script&#xD;&#xA;Follow this blog on the Fediverse&#xD;&#xA;Contact me:&#xD;&#xA;     Email&#xD;&#xA;     Github &#xD;&#xA;     Personal Mastodon&#xD;&#xA;     LinkedIn&#xD;&#xA;     Twitter&#xD;&#xA;&#xD;&#xA;!--emailsub--&#xD;&#xA;&#xD;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/dan-dimmock-3mt71MKGjQ0-unsplash.jpg" alt="Feature image"/></p>

<blockquote><p>This post is part of a series on m<a href="__GHOST_URL__/data-science-with-judgement-data-my-pdpc-decisions-journey/" rel="nofollow">y Data Science journey with PDPC Decisions</a>. Check it out for more posts on visualisations, natural languge processing, data extraction and processing!</p></blockquote>

<p> <strong>Update 13 June 2020:</strong> “At least until the PDPC breaks its website.” How prescient… about three months after I wrote this post, <a href="__GHOST_URL__/back-to-the-drawing-board/" rel="nofollow">the structure of the PDPC’s website was drastically altered</a>. The concepts and the ideas in this post haven’t changed, but the examples are outdated. This gives me a chance to rewrite this post. If I ever get round to it, I’ll provide a link.</p>

<p>Regular readers would already know that I try to run a <a href="https://github.com/houfu/pdpc-decisions" rel="nofollow">github repository</a> which tries to compile all personal data protection decisions in Singapore. Decisions are useful resources teeming with lots of information. They have statistics, insights into what factors are relevant in decision making and show that data protection is effective in Singapore. <a href="__GHOST_URL__/dpex-states-the-obvious-pdpa-enforcement-has-gone-up/" rel="nofollow">Even basic statistics about decisions make newspaper stories here locally.</a> It would be great if there was a way to mine all that information!</p>

<p><a href="https://github.com/houfu/pdpc-decisions" rel="nofollow">houfu/pdpc-decisionsData Protection Enforcement Cases in Singapore. Contribute to houfu/pdpc-decisions development by creating an account on GitHub.<img src="https://github.githubassets.com/favicons/favicon.svg" alt=""/>GitHubhoufu<img src="https://opengraph.githubassets.com/a322fd743f77362795e68b2d6c2f22d1c1baf93ba3bcda4fe230f1e3c0dceaa9/houfu/pdpc-decisions" alt=""/></a></p>

<p>Unfortunately, using the Personal Data Protection Commission in Singapore’s website to download judgements can be painful.</p>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/Screenshot_2019-12-02-Data-Protection-Enforcement-Cases.png" alt=""/>This is our target webpage today – Note the website has been transformed.</p>

<p>As you can see, you are only able to view no more than 5 decisions at one time. As the first decision dates back to 2016, you will have to go through several pages to grab everything! Actually just 23. I am sure you can do all that in 1 night, right? Right?</p>

<p>If you are not inclined to do it, then get your computer to do it. Using selenium, I wrote a python script to automate <em>the whole process</em> of finding all the decisions available on the website. What could have been a tedious night’s work was accomplished in <em><strong>34 seconds</strong></em>.</p>

<p>Check out the script here.</p>

<p>What follows here is a step by step write up of how I did it. So hang on tight!</p>

<h2 id="section-1-observe-your-quarry">Section 1: Observe your quarry</h2>

<p>Before setting your computer loose on a web page, it pays to understand the structure and inner workings of your web page. Open this up by using your favourite browser. For Chrome, this is <code>Developer&#39;s Tools</code> and in Firefox, this is <code>Web Developer</code>. You will be looking for a tab called <code>Sources</code>, which shows you the HTML code of the web page.</p>

<p>Play with the structure of the web page by hovering over various elements of the web page with your mouse. You can then look for the exact elements you need to perform your task:</p>
<ul><li>In order to see a new page, you will have to click on the page number in the pagination. This is under a section (a CSS class) called <code>group__pages</code>. Each page-number is under a section (another CSS class) called <code>page-number</code>.</li>
<li>Each decision has its own section (a CSS class) named <code>press-item</code>. The link to the download, which is either to a text file or a PDF file, is located in a link in each <code>press-item</code>.</li>
<li>Notice too that each <code>press-item</code> also has other metadata regarding the decision. For now, we are curious about the date of the decision and the respondent.</li></ul>

<h2 id="section-2-decide-on-a-strategy">Section 2: Decide on a strategy</h2>

<p>Having figured out the website, you can decide on how to achieve your goal. In this case, it would be pretty similar to what you would have done manually.</p>
<ol><li>Start on a page</li>
<li>Click on a link to download</li>
<li>Go to the next link until there are no more links</li>
<li>Move on to the next page</li>
<li>Keep repeating steps 1 to 4 until there are no more pages</li>
<li>Profit!</li></ol>

<p>Since we did notice the metadata, let’s use it. If you don’t use what is <em>already</em> in front of you, you will have to read the decision to extract such information In fact, we are going to use the metadata to name our decision.</p>

<h2 id="section-3-get-your-selenium-on-it">Section 3: Get your selenium on it!</h2>

<p><a href="https://selenium.dev/documentation/en/webdriver/" rel="nofollow">Selenium</a> drives a web browser. It mimics user interactions on the web browser, so our strategy in Step 2 is straightforward to implement. Instead of moving our mouse like we ordinarily would, we would tell the web driver what to do instead.</p>

<p><a href="https://www.selenium.dev/documentation/en/webdriver/" rel="nofollow">WebDriver :: Documentation for SeleniumDocumentation for Selenium<img src="https://www.selenium.dev/documentation/images/favicon.png" alt=""/>Selenium</a></p>

<p>Let’s translate our strategy to actual code.</p>

<h3 id="step-1-start-on-a-page">Step 1: <strong>Start on a page</strong></h3>

<p>We are going to need to start our web driver and get it to run on our web page.</p>

<p>    from selenium.webdriver import Chrome
    from selenium.webdriver.chrome.options import Options
    PDPC<em>decisions</em>site = “<a href="https://www.pdpc.gov.sg/Commissions-Decisions/Data-Protection-Enforcement-Cases%22" rel="nofollow">https://www.pdpc.gov.sg/Commissions-Decisions/Data-Protection-Enforcement-Cases&#34;</a>
    # Setup webdriver
    options = Options()
    # Uncomment the next two lines for a headless chrome
    # options.add<em>argument(&#39;—headless&#39;)
    # options.add</em>argument(&#39;—disable-gpu&#39;)
    # options.add<em>argument(&#39;—window-size=1920,1080&#39;)
    driver = Chrome(options=options)
    driver.get(PDPC</em>decisions_site)</p>

<h3 id="steps-2-download-the-file">Steps 2: Download the file</h3>

<p>Now that you have prepared your page, let’s drill down to the individual decisions itself. As we figured out earlier, each decision is found in a section named <code>press-item</code>. Get selenium to collect all the decisions on the page.</p>

<p>    judgements = driver.find<em>elements</em>by<em>class</em>name(&#39;press-item&#39;)</p>

<p>Recall that we were not just going to download the file, we will also be using the date of the decision and the respondent to name the file. For the date function, I found out that under each <code>press-item</code> there is a <code>press-date</code> which gives us the text of the decision date; we can easily convert this to a python <code>datetime</code> so we can format it anyway we like.</p>

<p>    def get<em>date(item: WebElement):
        item</em>date = datetime.strptime(item.find<em>element</em>by<em>class</em>name(&#39;press_<em>date&#39;).text, “%d %b %Y”)
        return item</em>date.strftime(“%Y-%m-%d”)</p>

<p>For the respondent, the heading (which is written in a fixed format and also happens to be the link to the download – score!) already gives you the information. Use a regular expression on the text of the link to suss it out. (One of the decisions do not follow the format of “Breach … by <em>respondent</em> “, so the alternative is also laid out)</p>

<p>    def get_respondent(item):
        text = item.text
        return re.split(r”\s+[bB]y|[Aa]gainst\s+“, text, re.I)[1]</p>

<p>You are now ready to download a file! Using the metadata and the link you just found, you can come up with meaningful names to download your files. Naming your own files will also help you avoid the idiosyncratic ways the PDPC names its own downloads.</p>

<p>Note that some of the files are not PDF downloads but instead are short texts in web pages. Using the earlier strategies, you can figure out what information you need. This time, I used BeautifulSoup to get the information. I did not want to use selenium to do any unnecessary navigation. Treat PDFs and web pages differently.</p>

<p>    def download<em>file(item, file</em>date, file<em>respondent):
        url = item.get</em>property(&#39;href&#39;)
        print(“Downloading a File: “, url)
        print(“Date of Decision: “, file<em>date)
        print(“Respondent: “, file</em>respondent)
        if url[-3:] == &#39;pdf&#39;:
            dest = SOURCE<em>FILE</em>PATH + file<em>date + &#39; &#39; + file</em>respondent + &#39;.pdf&#39;
            wget.download(url, out=dest)
        else:
            with open(SOURCE<em>FILE</em>PATH + file<em>date + &#39; &#39; + file</em>respondent + &#39;.txt&#39;, “w”) as f:
                from bs4 import BeautifulSoup
                from urllib.request import urlopen
                soup = BeautifulSoup(urlopen(url), &#39;html5lib&#39;)
                text = soup.find(&#39;div&#39;, class_=&#39;rte&#39;).getText()
                lines = re.split(r”ns+“, text)
                f.writelines([line + &#39;n&#39; for line in lines if line != “”])</p>

<h3 id="steps-3-to-5-download-every-item-on-every-page">Steps 3 to 5: Download every item on every page</h3>

<p>The next steps follow a simple idiom — <em>for every page and for every item on each page, download a file</em>.</p>

<p>    for page<em>count in range(len(pages)):
            pages[page</em>count].click()
            print(“Now at Page “, page<em>count)
            pages = refresh</em>pages(driver)
            judgements = driver.find<em>elements</em>by<em>class</em>name(&#39;press-item&#39;)
            for judgement in judgements:
                date = get<em>date(judgement)
                link = judgement.find</em>element<em>by</em>tag<em>name(&#39;a&#39;)
                respondent = get</em>respondent(link)
                download_file(link, date, respondent)</p>

<p>Unfortunately, once selenium changes a page, it needs to be refreshed. We are going to need a new <code>group__pages</code> and <code>page-number</code> in order to continue accessing the page. I wrote a function to “refresh” the variables I am using to access these sections.</p>

<p>    def refresh<em>pages(web</em>driver: Chrome):
        group<em>pages = web</em>driver.find<em>element</em>by<em>class</em>name(&#39;group_<em>pages&#39;)
        return group</em>pages.find<em>elements</em>by<em>class</em>name(&#39;page-number&#39;)
    . . .
    pages = refresh_pages(driver)</p>

<h2 id="conclusion">Conclusion</h2>

<p>Once you got your web driver to be thorough, you are done! In my last pass, 115 decisions were downloaded in 34 seconds. The best part is that you can repeat this any time there are new decisions. Data acquisition made easy! At least until the PDPC breaks its website.</p>

<h2 id="postscript-is-this-illegal">Postscript: Is this… Illegal?</h2>

<p><img src="https://raw.githubusercontent.com/houfu/lovelawrobots-content/master/2022/05/635520020160086182-country.jpg" alt=""/>I’m listening…</p>

<p>Web scraping has always been quite controversial and the stakes can be quite high. Copyright infringement, Misuse of Computer Act and trespass, to name a few. Funnily enough, manually downloading may be less illegal than using a computer. The PDPC’s own <a href="https://www.pdpc.gov.sg/Terms-and-Conditions" rel="nofollow">terms of use</a> is not on point at this.</p>

<p>( <strong>Update 15 Mar 2021</strong> : OK I think I am being fairly obtuse about this. There is a paragraph that states you can’t use robots or spiders to monitor their website. That might make sense in the past when data transfers were expensive, but I don&#39;t think that this kind of activity at my scale can crash a server.)</p>

<p>Personally, I feel this particular activity is fairly harmless — this is considered “own personal, non-commercial use” to me. I would likely continue with this for as long as I would like my own collection of decisions. Or until they provide better programmatic access to their resources.</p>

<p><a href="__GHOST_URL__/ready-to-mine-free-online-legal-materials-in-singapore-not-so-fast/" rel="nofollow">Ready to mine free online legal materials in Singapore? Not so fast!Amendments to Copyright Act might support better access to free online legal materials in Singapore by robots. I survey government websites to find out how friendly they are to this.<img src="__GHOST_URL__/favicon.png" alt=""/>Love.Law.Robots.Houfu<img src="__GHOST_URL__/content/images/2021/12/Email_Cover-1.jpg" alt=""/></a>In 2021, the Copyright Act in Singapore was amended to support data analysis, like web scraping? I wrote this follow-up post.</p>

<p><a href="https://write.as/houfu/tag:PDPC" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">PDPC</span></a>-Decisions <a href="https://write.as/houfu/tag:Programming" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Programming</span></a> <a href="https://write.as/houfu/tag:Python" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Python</span></a> <a href="https://write.as/houfu/tag:tutorial" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">tutorial</span></a> <a href="https://write.as/houfu/tag:Updated" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Updated</span></a></p>

<p><img src="https://res.cloudinary.com/lovelawrobots/image/upload/f_auto,q_auto/v1/blog-images/photo_2023-06-30_23-47-38_ozpz52" alt="Author Portrait" width="320"/>
<strong>Love.Law.Robots. – A blog by Ang Hou Fu</strong></p>
<ul><li><a href="https://remark.as/p/houfu/automate-boring-stuff-get-python-and-your-web-browser-to-download-your" rel="nofollow">Discuss...</a> this Post</li>
<li>If you found this post useful, or like my work, a tip is always appreciated:  </li>
<li>Follow [this blog on the Fediverse]()</li>
<li>Contact me:
<ul><li><a href="mailto:houfu@lovelawrobots.com" rel="nofollow">Email</a></li>
<li><a href="https://github.com/houfu" rel="nofollow">Github</a></li>
<li><a href="https://kopiti.am/@houfu" rel="nofollow">Personal Mastodon</a></li>
<li><a href="https://www.linkedin.com/in/hou-fu-ang-0a6851113/" rel="nofollow">LinkedIn</a></li>
<li><a href="https://twitter.com/houfu" rel="nofollow">Twitter</a></li></ul></li></ul>


]]></content:encoded>
      <guid>https://write.as/houfu/automate-boring-stuff-get-python-and-your-web-browser-to-download-your</guid>
      <pubDate>Mon, 02 Dec 2019 15:49:00 +0000</pubDate>
    </item>
  </channel>
</rss>