{"id":1302,"date":"2024-04-30T00:08:51","date_gmt":"2024-04-29T15:08:51","guid":{"rendered":"https:\/\/blog.peddals.com\/?p=1302"},"modified":"2024-05-27T01:01:08","modified_gmt":"2024-05-26T16:01:08","slug":"how-i-developed-speech-plus-subtitle-player-flet-app","status":"publish","type":"post","link":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/","title":{"rendered":"How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python."},"content":{"rendered":"\n<p>Flet can let you develop cool desktop apps in Python. I previously released an app that could play audio and display subtitles (SRT) simultaneously, as well as edit subtitles. How did I make it? Here\u2019s the background, steps, and code. The finished product is a standalone desktop app, and it\u2019s not overly complicated. However, using Python + Flet to create a single application from start to finish is not something you see often, so I hope this blog helps some Flet app developers! It\u2019s a long read, so I suggest searching word or using the table of contents rather than reading the whole post.<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-peddals-blog wp-block-embed-peddals-blog\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"2Pk7PuP3K8\"><a href=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/\">Speech + Subtitles (SRT) Player app made with Flet<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; clip: rect(1px, 1px, 1px, 1px);\" title=\"&#8220;Speech + Subtitles (SRT) Player app made with Flet&#8221; &#8212; Peddals Blog\" src=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/embed\/#?secret=AfAUcZmwFK#?secret=2Pk7PuP3K8\" data-secret=\"2Pk7PuP3K8\" width=\"525\" height=\"296\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Code and assets<\/h2>\n\n\n\n<p>The code, along with the Python code, logos for execution, and images for building are all stored on <a href=\"https:\/\/github.com\/tokyohandsome\/Speech-plus-Subtitles-Player\/blob\/main\/README.md\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Flet official documentations<\/h2>\n\n\n\n<p>If you\u2019re using Flet for the first time, please read the <a href=\"https:\/\/flet.dev\/docs\/\" target=\"_blank\" rel=\"noreferrer noopener\">official documentation<\/a> first. <\/p>\n\n\n\n<p>New releases are announced on the <a href=\"https:\/\/flet.dev\/blog\" target=\"_blank\" rel=\"noreferrer noopener\">official blog<\/a> and Discord, and other miscellaneous links can be found on the <a href=\"https:\/\/flet.dev\/support\/\" target=\"_blank\" rel=\"noreferrer noopener\">support page<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Background and things not technical<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Developer (myself) and background<\/h3>\n\n\n\n<p>I am an IT manager at a Japanese office of a global company. Programming is my hobby, and I have been creating small, unfinished, and experimental programs for several decades (in 8bit old-school BASIC, HyperCard\/HyperTalk, HTML\/JavaScript, and Python). I have read several introductory books on Python, but I have only read about 60-80% of each one. I tend to get bored and stop reading before the end because I start thinking about creating something new instead. In the past, I have used Tkinter and PySimpleGUI to create desktop applications, but I have not been satisfied with the results. Recently, I discovered Flet, which has a beautiful design and allows me to create desktop, web, and mobile applications with relative ease. I was so impressed that I started experimenting with it immediately. One day, I was amazed by OpenAI\u2019s Whisper, an excellent speech recognition tool, and on impulse, I started developing a subtitle editing app using Flet (I couldn\u2019t find anything similar in the market). Before that, I had created a <a href=\"https:\/\/blog.peddals.com\/en\/build-flet-app-for-macos\/\" target=\"_blank\" rel=\"noreferrer noopener\">password generator app<\/a> using Flet, which is available both as a desktop application and on <a href=\"https:\/\/blog.peddals.com\/flet-client-side-web-app\/\" target=\"_blank\" rel=\"noreferrer noopener\">the web<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">My development environment<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Mac (started with Mac mini M1 16GB RAM then migrated to a Mac Studio M2 Max 12-core CPU \/ 30-core GPU \/ 32GB RAM, refurbished)<\/li>\n\n\n\n<li>Keyboard: HHKB Pro 2 Type-S (USB only model)<\/li>\n\n\n\n<li>Mouse: Logi&#8217;s silent mouse<\/li>\n\n\n\n<li>Monitors: Dell 4K 27-inch and QHD 24-inch<\/li>\n\n\n\n<li>IDE: VisualStudio Code &#8211; Insiders (since the beginning I started using M1 mac mini)<\/li>\n\n\n\n<li>Version control: GitHub and GitHub Desktop<\/li>\n\n\n\n<li>Image generation: Mochi Diffusion (I used Keynote to design the app logo)<\/li>\n\n\n\n<li>Speech recognition\/text generation: MLX Whisper and a <a href=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/#Python_script_to_transcribe_audio_file_to_SRT_with_Whisper\" target=\"_blank\" rel=\"noreferrer noopener\">simple SRT generation code<\/a><\/li>\n\n\n\n<li>Files for test: <a href=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/#Download_online_video_as_m4a_audio_file\">m4a audio file generated by yt_dlp<\/a> and SRT text file as mentioned above.<\/li>\n\n\n\n<li>Memo, task management: Smartsheet (free account) and Apple default Memo app<\/li>\n\n\n\n<li>Hand writing\/drawing: Whiteboard notebook <a href=\"https:\/\/amzn.to\/49Mtf0k\" target=\"_blank\" rel=\"noreferrer noopener\">nu board<\/a> and <a href=\"https:\/\/amzn.to\/3Q0JwYn\" target=\"_blank\" rel=\"noreferrer noopener\">PILOT Board Master S<\/a> (links are amazon Japan pages)<\/li>\n\n\n\n<li>Well visited websites: Flet official website, Discord server, and Copilot free version<\/li>\n\n\n\n<li>Python: 3.11.7<\/li>\n\n\n\n<li>Flet: 0.21.2 (<code>pip install flet==0.21.2<\/code>)<\/li>\n\n\n\n<li>In my other blog posts you can find additional info around how to build Flet app <\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-peddals-blog wp-block-embed-peddals-blog\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"XLba3ERw90\"><a href=\"https:\/\/blog.peddals.com\/en\/build-flet-app-for-macos\/\">How to build Python GUI app on macOS with Flet<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; clip: rect(1px, 1px, 1px, 1px);\" title=\"&#8220;How to build Python GUI app on macOS with Flet&#8221; &#8212; Peddals Blog\" src=\"https:\/\/blog.peddals.com\/en\/build-flet-app-for-macos\/embed\/#?secret=mSmSa5URZa#?secret=XLba3ERw90\" data-secret=\"XLba3ERw90\" width=\"525\" height=\"296\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<figure class=\"wp-block-embed is-type-wp-embed is-provider-peddals-blog wp-block-embed-peddals-blog\"><div class=\"wp-block-embed__wrapper\">\n<blockquote class=\"wp-embedded-content\" data-secret=\"qdKE1q3scg\"><a href=\"https:\/\/blog.peddals.com\/en\/flet-successful-building-app-with-audio\/\">Successful build of Flet app with Audio control<\/a><\/blockquote><iframe loading=\"lazy\" class=\"wp-embedded-content\" sandbox=\"allow-scripts\" security=\"restricted\" style=\"position: absolute; clip: rect(1px, 1px, 1px, 1px);\" title=\"&#8220;Successful build of Flet app with Audio control&#8221; &#8212; Peddals Blog\" src=\"https:\/\/blog.peddals.com\/en\/flet-successful-building-app-with-audio\/embed\/#?secret=qf4Sw7XAEP#?secret=qdKE1q3scg\" data-secret=\"qdKE1q3scg\" width=\"525\" height=\"296\" frameborder=\"0\" marginwidth=\"0\" marginheight=\"0\" scrolling=\"no\"><\/iframe>\n<\/div><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">My development process<\/h2>\n\n\n\n<p>It was something like the list below. I thought of a plan and started implementing it, but Flet\u2019s implementation method wasn\u2019t clear to me and I couldn\u2019t make it work as expected. I spent several days going back and forth between reading the documentation and trying to write code, but I couldn\u2019t get it right. However, my motivation didn\u2019t drop even though I was stuck for a while. Instead, I took a break from Flet app development and worked on other things, like improving Whisper\u2019s recognition accuracy by adjusting parameters or imagining the target users and use cases of the app. After a few days, I came back to Flet with refreshed energy and enthusiasm, and I was able to complete the app at last.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>started my project by creating an audio control that looked like the official one.<\/li>\n\n\n\n<li>added a local audio file to the code and played it.<\/li>\n\n\n\n<li>drew an interface rough draft on a whiteboard (nu board) while imagining the app\u2019s features.<\/li>\n\n\n\n<li>implemented a slider that would move based on the audio playback status and displayed the elapsed time in the console. However, the slider did not move so I thought something was wrong with my code and spent some time to troubleshoot. It turned out the audio length was too long. Slider was already moving.<\/li>\n\n\n\n<li>in oppose to the above, added a feature that allowed users to move the slider which would play the audio from the designated position. This feature took several days to implement, but it was worth it.<\/li>\n\n\n\n<li>started by creating a <code>FilePicker<\/code> that would allow users to select audio files. This feature was quite straightforward and didn\u2019t require much effort. macOS remembers folder previously accessed. Nice.<\/li>\n\n\n\n<li>implemented the ability to automatically load any matching subtitles or transcripts when an audio file is selected. This feature was quite useful and made the app more convenient for users.<\/li>\n\n\n\n<li>added a function that would automatically generate a button from loaded subtitles by referencing to the <a href=\"https:\/\/flet.dev\/docs\/tutorials\/python-todo\">official To-Do app tutorial<\/a>. It was great seeing my app generating buttons.<\/li>\n\n\n\n<li>implemented millisecond and 00:00:00,000 format conversion logics. I used Copilot&#8217;s suggestions and started getting help from Copilot more frequently.<\/li>\n\n\n\n<li>rewrote the main part to a class. After that, I gradually understood the importance and meaning of Python classes.<\/li>\n\n\n\n<li>rewrote the entire code to use <code>async<\/code> functions. However, this did not improve the response time when there were many buttons on the screen. Later, Flet became async-first, and I had unknowingly taken the lead. Haha.<\/li>\n\n\n\n<li>implemented class interactions (e.g., playing audio based on the current flow, scrolling through subtitles, jumping to timestamp, etc.). I also relearned classes in practice.<\/li>\n\n\n\n<li>implemented file saving and loading functions. The operating system warns the user when there is a conflict with an existing file. It\u2019s very convenient.<\/li>\n\n\n\n<li>implemented a <code>SnackBar<\/code> to notify users if there was no file to load. This was easy to use and did not obstruct the interface.<\/li>\n\n\n\n<li>a bug occurred where the write dialog would not open and the app would stop doing anything. Since the issue was not reproducible but occurs rarely, I changed the export-as dialog to individual buttons instead.<\/li>\n\n\n\n<li>as an app, I prepared for release by finding free fonts for my logo and icon. I was simply too tired to investigate the cause and wanted to escape for a little while.<\/li>\n\n\n\n<li>added copyright information, made overall design adjustments, and prepared the app for release.<\/li>\n\n\n\n<li>found that built macOS app crashed due to NumPy, and I could not resolve. I logged an issue on <a href=\"https:\/\/github.com\/flet-dev\/flet\/issues\/2932\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a>.<\/li>\n\n\n\n<li>attempted to make a web app instead, but I couldn\u2019t open local files directly and gave up for now.<\/li>\n\n\n\n<li>released the app on GitHub and <a href=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/\">blogged<\/a> about it. At this moment the app could be run by <code>python main.py<\/code>.<\/li>\n\n\n\n<li>Copilot suggested a NumPy-free implementation, which I used to build the macOS app successfully.<\/li>\n\n\n\n<li>added the build process to the GitHub README and wrote a <a href=\"https:\/\/blog.peddals.com\/en\/flet-successful-building-app-with-audio\/\" target=\"_blank\" rel=\"noreferrer noopener\">blog post<\/a> about it. <\/li>\n\n\n\n<li>Finally, I started writing this article.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Overview<\/h2>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"862\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-1024x862.png\" alt=\"\" class=\"wp-image-912\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-1024x862.png 1024w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-300x252.png 300w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-768x646.png 768w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-1536x1292.png 1536w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/03\/screenshot_en-2048x1723.png 2048w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><figcaption class=\"wp-element-caption\">Completed app<\/figcaption><\/figure>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\">GUI layout<\/h3>\n\n\n\n<p>Please excuse the handwritten text and drawing. The whiteboard itself is an app (=<code>page<\/code>) and you can see that there is a large <code>column<\/code> in the middle of it, which contains the main class definition. The other two sections, <code>Audio<\/code> and <code>Dialogs<\/code>, are usually not displayed and are added to the page from the <code>main<\/code> function. Everything else is wrapped in <code>containers<\/code> or <code>rows<\/code> and added to the page from top to bottom.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/IMG_3220.heic\" alt=\"\" class=\"wp-image-1115\"\/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Code overview<\/h3>\n\n\n\n<p>Here&#8217;s a breakdown of the code by line number (xx-yy) and general content:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>(1-4) I&#8217;m importing Flet and other modules &#8211; I use <code>os<\/code> for path operations and <code>datetime<\/code> only to add dates to file names, so almost all the necessary elements and features of my app are being created using Flet alone.<\/li>\n\n\n\n<li>(6-79) Function block &#8211; conversion between milliseconds and digital format, and conversion of loaded text into a list for use within the app.<\/li>\n\n\n\n<li>(81-183) SubButton class that generates subtitle buttons from the list &#8211; Initialization, <code>build<\/code> method to layout text and buttons, and methods to process that performs various processing when the button is clicked.<\/li>\n\n\n\n<li>(185-791) The main <code>AudioSubPlayer<\/code> class of the app &#8211; first, in the initialization function (lines 187-374), all buttons, text fields, and other Flet controls used in the app&#8217;s layout are defined like <code>self.foobar<\/code>, and then in the next method block (lines 376-738), logics using <code>async<\/code> for events such as clicks are defined, and finally, in the <code>build<\/code> function (lines 740-791), the page layout is defined.<\/li>\n\n\n\n<li>(793-812) The <code>main<\/code> function &#8211; defining the basic structure of the window using <code>async<\/code>, and adding <code>audio<\/code> and <code>dialog<\/code> instances as overlays to the page.<\/li>\n\n\n\n<li>(815) Calls the <code>main<\/code> function<\/li>\n<\/ol>\n\n\n\n<p>I think there are unnecessary long parts in my code, but it seems that the Flet code tends to become lengthy.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">About SRT file format<\/h4>\n\n\n\n<p>The subtitle file format currently supported by this app is SRT. It&#8217;s a text file with the extension .srt. According to <a href=\"https:\/\/en.wikipedia.org\/wiki\/SubRip\" target=\"_blank\" rel=\"noreferrer noopener\">Wikipedia<\/a>, it originated from a text subtitle file format generated by SubRip, a Windows freeware. It was adopted because it was used in Whisper for speech-to-text conversion. You can find my <a href=\"https:\/\/blog.peddals.com\/en\/intro-to-speech-plus-subtitles-player-flet-app\/#Python_script_to_transcribe_audio_file_to_SRT_with_Whisper\" target=\"_blank\" rel=\"noreferrer noopener\">blog post here<\/a> about how to use Whisper to convert audio files into SRT format on macOS (with some simple Python code).<\/p>\n\n\n\n<p>The SRT file consists of 4 blocks for a subtitle text: index number, start time, &#8220;&#8211;&gt;&#8221;, end time, text and an empty line. Here is a sample of what this looks like (the beginning of Steve Jobs&#8217; famous speech):<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">1\n00:00:00,000 --&gt; 00:00:02,720\n Today, I want to tell you three stories from my life.\n\n2\n00:00:03,040 --&gt; 00:00:04,620\n That's it. No big deal.\n\n3\n00:00:04,980 --&gt; 00:00:06,160\n Just three stories.\n\n<\/pre>\n\n\n\n<p>The start and end times are in two digits for hours, minutes, and seconds, followed by the integer part of milliseconds after a comma. It should work fine if you use Whisper&#8217;s output, but this app does not support multiple lines for subtitles, so please combine them into one line if that&#8217;s the case. When using Whisper, blank lines with the same timestamp may be produced when speech recognition doesn&#8217;t work as intended; these are automatically removed when the file is read by this application.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Explanation of the code<\/h2>\n\n\n\n<p>From now on, I will explain the actual code and its explanation. I won&#8217;t go into much detail about Flet&#8217;s basic content, and I&#8217;ll proceed in an order that seems easier to understand. It would be helpful if you could open the code in an editor and\/or run the app while reading this.<\/p>\n\n\n\n<p>The Flet framework is imported at the beginning of the code as <code>ft<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The last line ft.app(target=main, assets_dir=&#8221;assets&#8221;) creates the app<\/h3>\n\n\n\n<p>This last line is creating the Flet app. With <code>target=main<\/code>, I&#8217;m specifying the main function as the app itself. <code>assets_dir=\"assets\"<\/code> sets the &#8216;assets&#8217; folder in the same directory as the code body to be used for storing files such as images that the app will use. If you&#8217;re going to build your code as as an executable app, I suggest to name the Flet app&#8217;s main file as <strong>main.py<\/strong>, the function name inside the code as <strong>main<\/strong>, and the folder name as <strong>assets<\/strong>, so when building the app, you can simply run <code>flet build macos<\/code> (for macOS).<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"815\"><code>ft.app(target=main, assets_dir=&quot;assets&quot;)<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;async def main&#8221; function to create a window and add overlay<\/h3>\n\n\n\n<p>This is a function that is called when the code is executed. It generates a <code>Page<\/code> instance, which serves as the foundation for the Flet app. After specifying the window title, initial size, and color theme, it adds an overlay with invisible audio files and dialogs to the page.<\/p>\n\n\n\n<p>At line 806, an instance of <code>AudioSubPlayer<\/code> is created, and a function called <code>load_audio<\/code> is passed in which adds audio files to the overlay. The next line appends this function to the page. This allows audio files to be added to the page from within the class.<\/p>\n\n\n\n<p>At lines 810-811, dialogs for opening and saving files are added as an overlay to the page using <code>overlay.extend()<\/code>.<\/p>\n\n\n\n<p>There might be alternative ways to manage overlays, but since adding overlays to a page couldn&#8217;t be achieved from within the <code>UserControl<\/code> class, I used this approach.<\/p>\n\n\n\n<p>Using&nbsp;<code>page.update()<\/code>, you update (redraw) the page controls. In Flet, if you make any visual changes, updating the relevant control will apply the changes to the GUI. If it\u2019s part of a larger process, you can update it at the end. So, for example, line 798 is unnecessary, my apologies (since I\u2019ve already included line numbers in various places in this post, I won\u2019t remove them prioritizing the text).<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"793\"><code># Main function that builds window and adds page. Also, adds audio file and dialogs that are invisible as overlay.\nasync def main(page: ft.Page):\n    page.title = &#39;Speech + Subtitles Player&#39;\n    page.window_height = 800\n    page.theme_mode=ft.ThemeMode.SYSTEM\n    page.update()\n\n    # Appends audio as an overlay to the page.\n    async def load_audio():\n        page.overlay.append(app.audio1)\n        page.update()\n\n    # Creates an instance of AudioSubPlayer class. Passes load_audio for the instance to append audio to the page. \n    app = AudioSubPlayer(load_audio)\n    page.add(app)\n\n    # Adds dialog instance methods to the page.\n    page.overlay.extend([app.pick_speech_file_dialog, app.pick_text_file_dialog, \n                         app.export_as_srt_dialog, app.export_as_txt_dialog])\n    page.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h3 class=\"wp-block-heading\">The main part of the app, &#8220;AudioSubPlayer&#8221; class<\/h3>\n\n\n\n<p>The main class is a custom control that inherits from <code>UserControl<\/code> and implements a user-defined control. The `build()` method, which is required by <code>UserControl<\/code>, is where UI is constructed. So, let&#8217;s take a look at its contents first (<a href=\"https:\/\/flet.dev\/blog\/flet-fastapi-and-async-api-improvements\/#custom-controls-api-normalized\" target=\"_blank\" rel=\"noreferrer noopener\">although this `UserControl` has been deprecated in Flet version 0.21.0<\/a>, it still works in my local version 0.21.2, so I&#8217;ll continue with the explanation). However, please note that there will likely be significant changes before the official major release, and when using a new framework, it&#8217;s essential to check the release notes for any breaking changes.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">UI layout by &#8220;def build(self)&#8221;<\/h4>\n\n\n\n<p>The block from the line 740 builds the user interface by constructing instances of <code>self.view<\/code> as an instance of <code>Column<\/code>. This is the largest hand-drawn diagram and its contents are within this column.<\/p>\n\n\n\n<p>As I wrote the code, I noticed that the layout was becoming increasingly complex. To make it easier to maintain in the future, I focused solely on defining the layout here and writing controls separately. This way, the <code>build()<\/code> method will be simpler and easier to read.<\/p>\n\n\n\n<p>In Flet, as you write the code for UI components, they will be stacked from top to bottom. Therefore, when you want to place multiple controls side by side, put them inside a Row and define their layout accordingly. For example, in the 748th line, there is a Row that contains a button to open an audio file and text displaying the file name, which will be displayed horizontally.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">While coding, I think it's a good idea to try out various properties (such as alignment and color elements) on separate lines separately  like lines 773-778. This way, you can easily add or comment out multiple properties. Once finalized, you can then combine all the properties onto one line like 771.<\/pre>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"740\" data-show-lang=\"1\"><code># === BUILD METHOD ===\ndef build(self):\n    self.view = ft.Column(expand=True, controls=[\n        ft.Container(content=\n            ft.Column(controls=[\n                ft.Row(controls=[\n                    self.base_dir,\n                ]),\n                ft.Row(controls=[\n                    self.speech_file_button,\n                    self.speech_file_name,\n                ]),\n                ft.Row(controls=[\n                    self.text_file_button,\n                    self.text_file_name,\n                    self.save_button,\n                    #self.export_button,\n                    self.export_as_srt_button,\n                    self.export_as_txt_button,\n                ]),\n                self.audio_slider,\n                ft.Row([\n                    self.position_text,\n                    self.duration_text,\n                ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),\n                ft.Row(controls=[\n                    self.rewind_button,\n                    self.play_button,\n                    self.faster_sw,\n                    self.sub_scroller_sw,\n                ]),\n            ]), expand=False, border_radius=10, border=ft.border.all(1), padding=10, \n        ),\n        ft.Container(content=\n            self.subs_view,\n            border_radius=10,\n            border=ft.border.all(1),\n            padding=5,\n        ),\n        ft.Row(controls=[\n            ft.Text(text_align=ft.CrossAxisAlignment.START,\n                    spans=[ft.TextSpan(&#39;\u00a9 2024 Peddals.com&#39;, url=&quot;https:\/\/blog.peddals.com&quot;)],\n                    ), \n            ft.Image(src=&#39;in_app_logo_small.png&#39;),\n        ],alignment=ft.MainAxisAlignment.SPACE_BETWEEN,\n        ),\n        ft.Container(content=\n            self.notification_bar)\n        ],\n        )\n\n    return self.view<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">In the method &#8220;def init(self, load_audio)&#8221; all controls are defined and initialized.<\/h4>\n\n\n\n<p>From line 187 onwards, the initialization part begins. First, class variables are initialized and a function for loading audio files is imported as mentioned earlier. The following lines up to 374 mainly consist of defining and initializing controls. While it would be quite extensive to explain each one individually, I can provide an overview: they define visual properties such as text or icons displayed on the control, along with methods that are called when specific events occur<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Typical definition of a Button control<\/h4>\n\n\n\n<p>As a common usage example, I will explain the contents of a button control definition for loading a text file.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"236\"><code>        # Open text file button\n        self.text_file_button = ft.ElevatedButton(\n            text=&#39;Open SRT\/TXT File&#39;,\n            icon=ft.icons.TEXT_SNIPPET_OUTLINED,\n            on_click=self.pre_pick_text_file,\n            disabled=True,\n            width=210,\n        )<\/code><\/pre><\/div>\n\n\n\n<p>First, please understand that these contents serve as initial values. The properties can be changed by other methods; thus, they define the application&#8217;s state at startup.<\/p>\n\n\n\n<p>(Line 237) An instance of <a href=\"http:\/\/ft.ElevatedButton\" target=\"_blank\" rel=\"noreferrer noopener\"><code>ft.ElevatedButton<\/code><\/a> is created with the name self.text_file_button (it might be difficult to notice in a dark theme, but it looks like a slightly raised button). Properties and methods are defined within parentheses using commas for separation.<\/p>\n\n\n\n<p>(Line 238) Define button display text using the <code>text<\/code> property.<\/p>\n\n\n\n<p>(Line 239) Specify an icon to be included in the button using the <code>icon<\/code> property. The position of the icon is fixed at the left end and&nbsp;cannot be changed. For reference on finding and confirming the name of the icon, please see below column.<\/p>\n\n\n\n<p>(Line 240) Define the method to be called when the <code>on_click<\/code> event occurs (i.e., when the button is clicked).<\/p>\n\n\n\n<p>(Line 241) At app startup, the button is disabled by setting the <code>disabled<\/code> property to <code>True<\/code>. After the audio file has been loaded, set it to&nbsp;<code>False<\/code> to make the button clickable.<\/p>\n\n\n\n<p>(Line 242) Fix the width of the button to 210 dots.<\/p>\n\n\n\n<p>Although we&#8217;re not using for this button, setting the <code>tooltip<\/code> property allows you to display notification text when you hover your mouse cursor&nbsp;over it.<\/p>\n\n\n\n<p>Note that I&#8217;ll come back to this later, but control&#8217;s properties can be set by methods or functions by doing like <code>self.text_file_button.disabled = False<\/code> along with <code>update<\/code>. For each control, refer to <a href=\"https:\/\/flet.dev\/docs\/controls\/\" target=\"_blank\" rel=\"noreferrer noopener\">the official documentation<\/a> for available properties, methods, and&nbsp;events.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Icons can be searched for on <a href=\"https:\/\/gallery.flet.dev\/icons-browser\/\" target=\"_blank\" rel=\"noreferrer noopener\">this icons browser page<\/a>. Unfortunately, as of this article's publication, clicking on displayed icons in Safari does not copy their names. You will need to use Chrome or manually enter icon name that appears on hover (Visual Studio Code will autocomplete icon names as well). If you're doing <code>import Flet as ft<\/code>, use <code>icon=ft.icons.THUMB_UP<\/code>.<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"195\" height=\"153\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image.png\" alt=\"\" class=\"wp-image-1171\"\/><figcaption class=\"wp-element-caption\">Hover on an icon to find its name if copy won&#8217;t work.<\/figcaption><\/figure>\n\n\n\n<p>Most controls and their properties should be easy to understand, but I found the process of opening or saving file using the <code>FilePicker<\/code> control wasn&#8217;t easy to follow. Therefore, I will explain it separately.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"187\"><code>    def __init__(self, load_audio):\n        super().__init__()\n        self.position = 0\n        self.duration = 0\n        self.isPlaying = False\n        self.load_audio = load_audio\n\n        # == Controls ==\n        \n        # Audio control with default properties\n        self.audio1 = ft.Audio(\n            src=&#39;&#39;,\n            volume=1,\n            balance=0,\n            playback_rate=1,\n            on_loaded=self.loaded,\n            on_position_changed = self.position_changed,\n            on_state_changed = self.playback_completed,\n        )\n\n        # Path of the audio file\n        self.base_dir = ft.Text(value=f&quot;Base Directory: &quot;)\n\n        # Open speech file button\n        self.speech_file_button = ft.ElevatedButton(\n            text=&#39;Open Speech File&#39;, \n            icon=ft.icons.RECORD_VOICE_OVER_OUTLINED, \n            width=210,\n            on_click=self.pre_pick_speech_file,\n        )\n\n        # Speech file picker control\n        self.pick_speech_file_dialog = ft.FilePicker(on_result=self.pick_speech_file_result)\n\n        # Speech file name\n        self.speech_file_name = ft.Text(value=&#39;\u2190 Click to open a speech file.&#39;)\n\n        # Alert dialog that opens if subtitle was edited but not saved when Open Speech File button is clicked.\n        self.speech_save_or_cancel_dialog = ft.AlertDialog(\n            modal=True,\n            title=ft.Text(&#39;Change not saved.&#39;),\n            content=ft.Text(&#39;Do you want to discard the change?&#39;),\n            actions=[\n                #ft.TextButton(&#39;Save&#39;, on_click=self.save_then_open, tooltip=&#39;Save then open another file.&#39;),\n                ft.TextButton(&#39;Open without save&#39;, on_click=self.open_speech_without_save, tooltip=&#39;Change will be lost.&#39;),\n                ft.TextButton(&#39;Cancel&#39;, on_click=self.close_speech_save_or_cancel_dialog),\n            ]\n        )\n\n        # Open text file button\n        self.text_file_button = ft.ElevatedButton(\n            text=&#39;Open SRT\/TXT File&#39;,\n            icon=ft.icons.TEXT_SNIPPET_OUTLINED,\n            on_click=self.pre_pick_text_file,\n            disabled=True,\n            width=210,\n        )\n        \n        # Text file picker control\n        self.pick_text_file_dialog = ft.FilePicker(on_result=self.pick_text_file_result)\n\n        # Text file name\n        self.text_file_name = ft.Text(value=&#39;No file selected.&#39;)\n\n        # Save button to update edited subtitles. No dialog, it just overwrites current text file.\n        self.save_button = ft.ElevatedButton(\n            text=&#39;Save&#39;, \n            icon=ft.icons.SAVE_OUTLINED, \n            tooltip=&#39;Update current SRT\/TXT file.&#39;,\n            disabled=True,\n            on_click=self.save_clicked\n            )\n        \n        # Export as SRT button which opens a save dialog. Only available when SRT is open because SRT needs timestamp.\n        self.export_as_srt_button = ft.ElevatedButton(\n            text = &#39;SRT&#39;,\n            icon=ft.icons.SAVE_ALT,\n            on_click=self.export_as_srt,\n            disabled=True,\n            tooltip=&#39;Export as SRT file.&#39;\n        )\n\n        # Export as SRT file picker\n        self.export_as_srt_dialog = ft.FilePicker(on_result=self.export_as_srt_result)\n\n        # Export as TXT button which opens a save dialog. TXT has not timestamp, subtitle text only.\n        self.export_as_txt_button = ft.ElevatedButton(\n            text = &#39;TXT&#39;,\n            icon=ft.icons.SAVE_ALT,\n            on_click=self.export_as_txt,\n            disabled=True,\n            tooltip=&#39;Export as TXT file.&#39;\n        )\n\n        # Export as TXT file picker\n        self.export_as_txt_dialog = ft.FilePicker(on_result=self.export_as_txt_result)\n\n        # Export button to open a dialog (not in use)\n        self.export_button = ft.ElevatedButton(\n            text=&#39;Export as...&#39;, \n            icon=ft.icons.SAVE_ALT, \n            on_click=self.open_export_dialog,\n            disabled=True,\n            )\n        \n        # Export as dialog (not in use)\n        self.export_dialog = ft.AlertDialog(\n            modal = True,\n            title = ft.Text(&#39;Export text as...&#39;),\n            content = ft.Text(&#39;Plesae select a file type.&#39;),\n            actions = [\n                ft.TextButton(&#39;SRT&#39;, on_click=self.export_as_srt, tooltip=&#39;Subtitles with timestamps&#39;),\n                ft.TextButton(&#39;TXT&#39;, on_click=self.export_as_txt, tooltip=&#39;Subtitles only (no timestamps)&#39;),\n                #ft.TextButton(&#39;CSV&#39;, on_click=self.export_csv, tooltip=&#39;Comma separated value&#39;),\n                # I guess no one needs subtitles in CSV...\n                ft.TextButton(&#39;Cancel&#39;, on_click=self.close_export_dialog),\n            ],\n            actions_alignment=ft.MainAxisAlignment.SPACE_BETWEEN,\n        )\n        \n        # Alert dialog that opens if subtitle was edited but not saved when Open SRT\/TXT File button is clicked.\n        self.text_save_or_cancel_dialog = ft.AlertDialog(\n            modal=True,\n            title=ft.Text(&#39;Change not saved.&#39;),\n            content=ft.Text(&#39;Do you want to discard the change?&#39;),\n            actions=[\n                #ft.TextButton(&#39;Save&#39;, on_click=self.save_then_open, tooltip=&#39;Save then open another file.&#39;),\n                ft.TextButton(&#39;Open without save&#39;, on_click=self.open_text_without_save, tooltip=&#39;Change will be lost.&#39;),\n                ft.TextButton(&#39;Cancel&#39;, on_click=self.close_text_save_or_cancel_dialog),\n            ]\n        )\n        # Audio position slider\n        self.audio_slider = ft.Slider(\n            min = 0,\n            value = int(self.position\/10000),\n            label = &quot;{value}ms&quot;,\n            on_change = self.slider_changed,\n        )\n\n        # Current playing position and duration of audio file\n        self.position_text = ft.Text(value=&#39;Current position&#39;)\n        self.duration_text = ft.Text(value=&#39;Duration (hh:mm:ss,nnn)&#39;)\n        \n        # Rewinds 5 seconds\n        self.rewind_button = ft.ElevatedButton(\n            icon=ft.icons.REPLAY_5,\n            text=&quot;5 secs&quot;,\n            tooltip=&#39;Rewind 5 secs&#39;,\n            on_click=self.rewind_clicked,\n            disabled=True,\n        )\n\n        # Play\/Pause button. After loading audio file, this button will always be focused (space\/enter to play\/pause).\n        self.play_button = ft.ElevatedButton(\n            icon=ft.icons.PLAY_ARROW,\n            text = &quot;Play&quot;,\n            on_click=self.play_button_clicked,\n            disabled=True,\n        )\n\n        # 1.5x faster toggle switch\n        self.faster_sw = ft.Switch(\n            label=&#39;1.5x&#39;,\n            value=False,\n            on_change=self.playback_rate,\n        )\n\n        # Auto scroll toggle switch\n        self.sub_scroller_sw = ft.Switch(\n            label=&#39;Auto scroll&#39;,\n            value=True,\n        )\n                \n        # Area to add subtitles as buttons\n        self.subs_view = ft.Column(\n            spacing = 5,\n            height= 400,\n            width = float(&quot;inf&quot;),\n            scroll = ft.ScrollMode.ALWAYS,\n            auto_scroll=False,\n        )\n\n        # Notification bar control at the bottom\n        self.notification_bar=ft.SnackBar(\n            content=ft.Text(&#39;Speech + Subtitle Player&#39;),\n            duration=2000,\n            bgcolor=ft.colors.BLUE_GREY_700,\n        )<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Class Method (Logic) Part<\/h4>\n\n\n\n<p><a href=\"https:\/\/flet.dev\/blog\/flet-fastapi-and-async-api-improvements\/#async-first-framework\" target=\"_blank\" rel=\"noreferrer noopener\">From version 0.21.0, Flet has become an async-first framework<\/a>, and it is recommended to create functions or methods in the form of <code>async def<\/code> unless synchronous processing is necessary. This can improve the responsiveness of your app, making it easier to manage without worrying about the details. Personally, I unfortunately started using async (<code>await<\/code> and <code>control.update_async()<\/code>) with an earlier version, then upgraded to a later async-first Flet version, and found I had to rewrite a lot of code\u2026 anyway, from line 378 to 738, most of the methods are defined as <code>async def<\/code>, and <code>self.update()<\/code> is used to update the view.<\/p>\n\n\n\n<p>Below, I&#8217;ll describe some of the methods that I&#8217;d like to explain.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Process after completion of loading audio file, async def loaded(self, e)<\/h4>\n\n\n\n<p>The method from line 378 is called when an audio file has been loaded. It contains various changes to properties, as well as conversion processing for using subtitle files within the app. This is the longest single method in the entire code.<\/p>\n\n\n\n<p>The first 30 lines or so are quite straightforward, setting values for properties of controls such as sliders, text, and buttons. The first three lines do something like this.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"379\"><code>self.audio_slider.max = int(await self.audio1.get_duration_async())<\/code><\/pre><\/div>\n\n\n\n<p>We are using the <code>get_duration_async()<\/code> method of the Audio control to retrieve the duration (in milliseconds) of the audio, and assigning it to the <code>max<\/code> property of the slider control <code>audio_slider<\/code>. In Flet version 0.21.2, when using a method that returns a value like this, we<br>need to use the <code>await ~ &lt;method&gt;_async()<\/code> syntax, which is different from other parts of the code.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"380\"><code>self.duration_text.value = f&#39;{ms_to_hhmmssnnn(self.audio_slider.max)}&#39;<\/code><\/pre><\/div>\n\n\n\n<p>We are taking the milliseconds obtained earlier and converting it to the format &#8220;00:00:00,000&#8221; for display as a text on the right side of the slider. The function <code>ms_to_hhmmssnnn()<\/code> (which I got from Copilot) is used for this conversion.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"381\"><code>self.audio_slider.divisions = self.audio_slider.max\/\/1000<\/code><\/pre><\/div>\n\n\n\n<p>We are dividing the slider into 1-second intervals (1000 milliseconds) using its <code>divisions<\/code> property. This is because the slider does not display numerical values unless it is divided. Furthermore, Flet only generates audio playback events at 1-second intervals, so we do this to match that timing. In reality, since the slider&#8217;s value cannot be changed from the millisecond display in this app, displaying the numerical value has little significance here.<\/p>\n\n\n\n<p>The next <code>if<\/code> block (from line 383) handles processing when a subtitle file is found. The <code>create_subtitles()<\/code> function processes the subtitle file internally and stores it in a list format in <code>self.subtitles<\/code>. When a text file (.txt) is loaded, it does not contain timestamps, so all timestamps are stored as 55:55:55,555 (20135.55) seconds for simplicity&#8217;s sake. This value can be referenced throughout the code where necessary. There&#8217;s no specific reason to select fives, but reading a 56-hour audio file is unlikely.<\/p>\n\n\n\n<p>The code from lines 397 to 406 mainly focuses on making buttons for audio playback clickable. In this app, audio playback and pause buttons are usually focused by default, allowing users to control playback with space or enter keys. Initially, I wanted to focus on the Open Speech File button at startup, then switch to the play\/pause button once a file is loaded, but it didn&#8217;t work out that way. Some leftover code from this attempt remains in lines 398-403.<\/p>\n\n\n\n<p>The code from lines 408 to 433 processes a list of subtitle files already generated, adjusting various settings for both TXT files without timestamps and SRT files with timestamps. For each subtitle line, a button is created. The actual content of the button is created in another<br>class <code>SubButton()<\/code>, but here, an instance named <code>sub<\/code> is assigned and appended to the controls list of the app&#8217;s bottom half screen area using <code>self.subs_view.controls.append(sub)<\/code>.<\/p>\n\n\n\n<p>The code from lines 436 to 443 displays a message at the bottom of the screen depending on whether a subtitle file exists. The <code>self.open_notification_bar<\/code> method is used, which takes only text and simply sends a notification when called. In cases where a subtitle file was not found, it is called with the type set to &#8216;error&#8217; and displayed for a longer time in an error color.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"377\"><code>    # Called once audio file is loaded. Enable\/disable buttons, create subtitles list, etc.\n    async def loaded(self, e):\n        self.audio_slider.max = int(await self.audio1.get_duration_async())\n        self.duration_text.value = f&#39;{ms_to_hhmmssnnn(self.audio_slider.max)}&#39;\n        self.audio_slider.divisions = self.audio_slider.max\/\/60\n        # Enables buttons if associated text file exists.\n        if self.text_file != &#39;No Text File.&#39;:\n            # Call function to create the list of subtitles, self.subtitles.\n            self.subtitles = create_subtitles(self.text_file)\n            self.save_button.text = &#39;Save&#39;\n            self.save_button.disabled=False\n            self.export_button.disabled=False\n            self.export_as_srt_button.disabled=False\n            self.export_as_txt_button.disabled=False\n        # Disable buttons if associated text file does not eixt.\n        else:\n            self.save_button.disabled=True\n            self.export_button.disabled=True\n            self.export_as_srt_button.disabled=True\n            self.export_as_txt_button.disabled=True\n            self.subtitles = []\n        self.speech_file_button.autofocus=False\n        self.speech_file_button.update()\n        self.play_button.disabled=False\n        self.play_button.focus()\n        self.play_button.autofocus=True\n        self.play_button.update()\n        self.rewind_button.disabled=False\n        self.text_file_button.disabled=False\n        self.subs_view.controls.clear()\n        \n        # Create buttons of subtitles from the list self.subtitles.\n        if self.subtitles != []:\n            # .txt or .srt file\n            for i in range(len(self.subtitles)):\n                index = self.subtitles[i][0]\n                start_time = self.subtitles[i][1]\n                # .txt file (timestap is dummy, 55:55:55,555) disable buttons.\n                if self.subtitles[0][1]== 201355555:\n                    self.sub_scroller_sw.value=False\n                    self.sub_scroller_sw.disabled=True\n                    self.export_dialog.actions[0].disabled=True\n                    self.export_as_srt_button.disabled=True\n                # .srt file\n                else:\n                    self.sub_scroller_sw.value=True\n                    self.sub_scroller_sw.disabled=False\n                self.sub_scroller_sw.update()\n                end_time = self.subtitles[i][2]\n                text = self.subtitles[i][3]\n                \n                # Create button instance of each subtitle. Include methods and controls for the instance to call or update.\n                sub = SubButton(index, start_time, end_time, text, self.sub_time_clicked, self.play_button, \n                                self.save_button, self.subtitles)\n\n                # Add button to the subtitle button area, subs_view.\n                self.subs_view.controls.append(sub)\n\n            # Call snackbar to show a notification.\n            notification = f&#39;Subtitle file loaded: {os.path.basename(self.text_file)}&#39;\n            await self.open_notification_bar(notification)\n        \n        # No text file found. Call snackbar to show an alert.\n        else:\n            notification = f&#39;Subtitle file (.srt or .txt) not found.&#39;\n            await self.open_notification_bar(notification, type=&#39;error&#39;)\n            print(&#39;Subtitle file not found.&#39;)\n\n        self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Method when playback position changes, async def position_changed(self, e)<\/h4>\n\n\n\n<p>The methods from lines 447 to 454 are called when the playback position of an audio file changes, specifically when the <code>on_position_changed<\/code> event of <code>self.audio1<\/code> occurs. In concrete cases, this will be triggered automatically every second during playback, and also when the user manually moves the slider or clicks on a timestamp in other situations. Let&#8217;s take a look at the code.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"449\"><code>self.audio_slider.value = e.data\n#print(&quot;Position:&quot;, self.audio_slider.value)\nself.position_text.value = ms_to_hhmmssnnn(int(e.data))<\/code><\/pre><\/div>\n\n\n\n<p>The <code>on_position_changed<\/code> property receives an argument <code>e<\/code> within a method. The value of <code>e.data<\/code> is the playback position (elapsed time) in milliseconds, so this value is assigned to the <code>value<\/code> property of the <code>audio_slider<\/code> control to update its position. Additionally, the converted value is inserted into the <code>value<\/code> property of the <code>position_text<\/code> control, which will display a readable format and appear on the left side of the slider.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"171\" height=\"88\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-8.png\" alt=\"\" class=\"wp-image-1300\"\/><\/figure>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"452\"><code>if (self.sub_scroller_sw.value == True) and (self.text_file_name.value != &#39;No Text File.&#39;):\n   self.scroll_to(self.audio_slider.value)\nself.update()<\/code><\/pre><\/div>\n\n\n\n<p>The code above checks two conditions: the state of the auto-scroll switch for subtitles and whether a subtitle file exists. If no subtitle file is loaded, it displays &#8220;No Text File.&#8221; and uses this as a flag itself. When both conditions are true, it calls the <code>scroll_to<\/code> method to scroll the subtitles, passing <code>self.audio_slider.value<\/code> as an argument. Finally, <code>self.update()<\/code> updates the playback time of this method itself.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Process when the slider position changes, async def slider_changed(self, e)<\/h4>\n\n\n\n<p>The method from lines 457 to 460 is called when the slider position changes, specifically when the <code>on_change<\/code> method of <code>self.audio_slider<\/code> control is triggered.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"457\"><code>self.audio1.seek(int(self.audio_slider.value))<\/code><\/pre><\/div>\n\n\n\n<p>The <code>seek<\/code> method of <code>self.audio1<\/code> is called with the value of the slider (<code>self.audio_slider.value<\/code>) to change the playback position. After that, it&#8217;s just a matter of updating; changing the audio playback position is extremely simple.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Play button, async def play_button_clicked(self, e) and async def playback_completed(self, e)<\/h4>\n\n\n\n<p>The code from lines 463 to 488 handles processing related to the Play button. When an audio file is loaded, during playback, when paused, and after playback has ended, each state uses methods of <code>self.audio1<\/code> to control playback or pause through button clicks. Additionally, icons and text are also updated accordingly.<\/p>\n\n\n\n<p>I thought would be possible to get the playing status (e.g., &#8220;playing&#8221;) from <code>e.data<\/code>, but unfortunately, it didn&#8217;t work out. Instead, I created a class variable <code>self.isPlaying<\/code> to determine the state. Although the button could have displayed the same content consistently, such as &#8220;(Play \/ Pause)&#8221;, I wanted to display icons that change depending on the situation, which also came in handy during debugging when I wanted to see the status.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"426\"><code>    # Change Play\/Pause status and icon when called.\n    async def play_button_clicked(self, e):\n        self.position = await self.audio1.get_current_position_async()\n        if (self.isPlaying == False) and (self.position == 0):\n            self.audio1.play()\n            self.isPlaying = True\n            self.play_button.icon=ft.icons.PAUSE\n            self.play_button.text = &quot;Playing&quot;\n        elif self.isPlaying == False:\n            self.audio1.resume()\n            self.isPlaying = True\n            self.play_button.icon=ft.icons.PAUSE\n            self.play_button.text = &quot;Playing&quot;\n        else:\n            self.audio1.pause()\n            self.isPlaying = False\n            self.play_button.icon=ft.icons.PLAY_ARROW\n            self.play_button.text = &quot;Paused&quot;\n        self.update()\n    \n    # When audio playback is complete, reset play button and status.\n    async def playback_completed(self, e):\n        if e.data == &quot;completed&quot;:\n            self.isPlaying = False \n            self.play_button.icon=ft.icons.PLAY_ARROW\n            self.play_button.text = &quot;Play&quot;\n        self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Rewind and 1.5x speed, async def rewind_clicked(self, e) and async def playback_rate(self, e)<\/h4>\n\n\n\n<p>The code from lines 491 to 507 handles processing for the rewind button and the 1.5x playback speed switch. The rewind function is a simple one that ensures the value doesn&#8217;t become negative. The 1.5x playback speed is also straightforward, simply assigning 1.5 to the <code>playback_rate<\/code> method of the Audio control when the switch is on. Note that after changing the speed, it&#8217;s necessary to update the Audio control using <code>await self.audio1.update_async()<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">As a fundamental principle of app design, I aimed to create a simple and intuitive design that is easy to operate. I also made sure to only add necessary features. The rewind button is one such feature. In my own experience, when editing subtitles, I often forget to pause playback temporarily. Moreover, subtitles typically appear at the top while playing, so it's convenient to have a button that allows me to go back a little bit during playback. If needed, I can click it\nmultiple times to rewind further. The reason why I didn't use 3 or 6 seconds is simply because there are no icons available for those numbers.\nThe 1.5x speed switch is designed with the trend of shortening time. I did try using 2x speed, but personally felt it was a bit too extreme, so I settled on 1.5x instead. For iOS and macOS, there's a limitation to playback rates within the range of 0.5 to 2, so you may want to experiment with changing <code>self.audio1.playback_rate = 1.5<\/code> depending on your needs or target users.\n\n<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"366\" height=\"78\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-2.png\" alt=\"\" class=\"wp-image-1218\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-2.png 366w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-2-300x64.png 300w\" sizes=\"auto, (max-width: 366px) 100vw, 366px\" \/><\/figure>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"490\"><code>    # When 5 secs button is clicked, rewind 5 seconds.\n    async def rewind_clicked(self, e):\n        if self.audio_slider.value &lt;= 5*1000:\n            self.audio_slider.value = 0\n        else:\n            self.audio_slider.value -= 5*1000\n        self.audio1.seek(int(self.audio_slider.value))\n        #print(int(self.audio_slider.value))\n        self.update()\n    \n    # Switch playback rate between normal and 1.5x faster.\n    async def playback_rate(self, e):\n        if self.faster_sw.value == True:\n            self.audio1.playback_rate = 1.5\n        else:\n            self.audio1.playback_rate = 1\n        #print(f&#39;Playback rate: {self.audio1.playback_rate}&#39;)\n        await self.audio1.update_async()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Timestamp buttons, async def sub_time_clicked(self, start_time)<\/h4>\n\n\n\n<p>The code from lines 510 to 514 handles processing when the timestamp button is clicked after loading an SRT file. When the button is clicked, it plays the part of the time corresponding to that timestamp. If playback has been paused, it will resume playback.<\/p>\n\n\n\n<p>The timestamp buttons are generated by another class <code>SubButton<\/code>. When an instance of this class is created, it passes this method to be called when the button is clicked. The button then receives its own <code>start_time<\/code> from the <code>jump_clicked()<\/code> method of the <code>SubButton<\/code> class and uses the <code>seek<\/code> method of the Audio control to jump to that time.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"218\" height=\"138\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-3.png\" alt=\"\" class=\"wp-image-1224\"\/><\/figure>\n\n\n\n<p>Let&#8217;s go through the code and its explanation step by step.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"428\"><code># Create button instance of each subtitle. Include methods and controls for the instance to call or update.\nsub = SubButton(index, start_time, end_time, text, self.sub_time_clicked, self.play_button, \n                self.save_button, self.subtitles)<\/code><\/pre><\/div>\n\n\n\n<p>The instance creation code section. This method <code>self.sub_time_clicked<\/code> is being passed as an argument.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-line=\"87,91\" data-start=\"81\"><code># Create button of subtitle text.\nclass SubButton(ft.UserControl):\n    def __init__(self, index, start_time, end_time, text, sub_time_clicked, play_button, save_button, subtitles):\n        super().__init__()\n        # Parameter of each subtitle.\n        self.index = index\n        self.start_time = start_time\n        self.end_time = end_time\n        self.text = text\n        # Passed methods and controls to call and update.\n        self.sub_time_clicked = sub_time_clicked<\/code><\/pre><\/div>\n\n\n\n<p>The initialization part of another class <code>SubButton<\/code> that creates a button (only first part). This class is storing objects passed from its parent class as its own object, which are highlighted in this section.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"181\"><code># When timestamp clicked calls AudioSubPlayer.sub_time_clicked to jump to button position.\nasync def jump_clicked(self, e):\n    await self.sub_time_clicked(self.start_time)<\/code><\/pre><\/div>\n\n\n\n<p>This is the method that is called when the timestamp button is clicked as a result of an <code>on_click<\/code> event. This method uses <code>self.start_time<\/code> and <code>self.sub_time_clicked<\/code> to execute a method from its parent class.<\/p>\n\n\n\n<p>And finally, this method plays the audio at the position of <code>start_time<\/code>.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\" data-start=\"509\"><code># When the timestamp is clicked, jump to its position and play if not playing.\nasync def sub_time_clicked(self, start_time):\n    self.audio1.seek(int(start_time))\n    if self.isPlaying == False:\n        await self.play_button_clicked(start_time)\n    self.update()<\/code><\/pre><\/div>\n\n\n\n<pre class=\"wp-block-preformatted\">Even if you understand Python and classes, it took me a long time to figure out how to execute a method from an instance of a class. It wasn't just a simple Google search away, as I struggled to translate my intentions into searchable keywords. Those who are stuck in their ways like GOTO\/GOSUB, which are extinct species of knowledge, I highly recommend thoroughly studying Python classes.<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Scroll of subtitle buttons, async def scroll_to(self, e)<\/h4>\n\n\n\n<p>The code from lines 517 to 525 is scrolling subtitles. This method is only called when a time-stamped SRT file is opened, specifically from the <code>position_changed<\/code> method. The argument <code>e<\/code> passed to this method contains the playback position (in milliseconds) of the audio. The class variable <code>self.subtitles<\/code> is a 2D list where each inner list contains consecutive index number, start times, end times, and text. This method references the index <code>index<\/code> and end time <code>end_time<\/code>.<\/p>\n\n\n\n<p>What I wanted to do here was move the subtitles corresponding to the currently playing audio to the top. However, Flet can only retrieve the playback position of an audio file once per second, so it scrolls to the subtitle button with the end time closest to that value if it&#8217;s larger than the current playback position. This may not be perfectly synchronized in real-time, but it will ensure that the currently playing subtitles are displayed either at the top or the second position.<\/p>\n\n\n\n<p>Let&#8217;s go through the code and explanation step by step.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"518\"><code>end_time = [item[2] for item in self.subtitles]<\/code><\/pre><\/div>\n\n\n\n<p>The local variable <code>end_time<\/code> of type list is assigned with all the end times of the subtitles.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"522\"><code>index = min(range(len(end_time)), key=lambda i: abs(end_time[i]-e))<\/code><\/pre><\/div>\n\n\n\n<p>The local variable <code>index<\/code> is assigned with the position of the subtitle that has the end time closest to the current playback position. The index is an integer starting from 0.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"523\"><code>key=str(self.subtitles[index][0])<\/code><\/pre><\/div>\n\n\n\n<p>The local variable <code>key<\/code> is assigned with the index number from the SRT file converted to a string. The index numbers in the SRT file start from 1 and are not necessarily consecutive, so I&#8217;ve added an extra step to consider the possibility of missing index numbers (in reality, after writing this code, I generated the subtitles list inside the app using a code that ensures the indices become consecutive, making <code>key = str(index+1)<\/code> have the same reslut).<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"524\"><code>self.subs_view.scroll_to(key=key, duration =1000)<\/code><\/pre><\/div>\n\n\n\n<p>The <code>scroll_to<\/code> method of the <code>subs_view<\/code> instance, which is a Column object, is used to scroll to the button with the index number equal to the local variable <code>key<\/code>, with a smoothness of 1000 milliseconds (1 second). The left-hand side <code>key<\/code> refers to the property of the <code>scroll_to<\/code> method, while the right-hand side <code>key<\/code> is the local variable holding the index number as a string.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">I'd like to add two points to this explanation. The first point is that <strong>if you have many buttons, the Flet app's performance can become sluggish<\/strong>. In particular, if you have over 300 buttons, the window movement can behave strangely. This is not a problem with CPU or memory usage, but rather a specification-related issue in Flet. If you're planning to create an app that uses many lists, I think it would be better to consider using a different control. When I researched this, I couldn't find any other controls that allow scrolling and <code>on_click<\/code> event handling, but there may be some workaround.\n\nThe second point is related to NumPy usage. In Flet version 0.21.2 on macOS, if you use NumPy in your code (as I did initially at line 520), the built app will crash when run. This is a problem that occurs only when building for macOS. I rewrote my code to avoid this issue, and I've written about it in a <a href=\"https:\/\/blog.peddals.com\/en\/flet-successful-building-app-with-audio\/\" target=\"_blank\" rel=\"noreferrer noopener\">separate article<\/a>. \u2192 NumPy issue is resolved by Flet. <a href=\"https:\/\/blog.peddals.com\/en\/flet-build-macos-now-works-with-numpy\/\" target=\"_blank\" rel=\"noreferrer noopener\">See this post<\/a>.<\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Loading audio file, async def pre_pick_speech_file(self, e), and related processes<\/h4>\n\n\n\n<p>From here, I will explain the methods and controls related to loading an audio file. While utilizing OS features makes things easier, it seems that Flet or <a href=\"https:\/\/flet.dev\/docs\/controls\/filepicker\/\" target=\"_blank\" rel=\"noreferrer noopener\">FilePicker control<\/a> is not sufficient for implementing &#8220;Open File&#8221; and &#8220;Save File&#8221; capabilities. Many additional elements are necessary to achieve this. Specifically, when opening a file, you basically need to do the following:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create an instance of the dialog control.<\/li>\n\n\n\n<li>Add it to the page.<\/li>\n\n\n\n<li>Create a button that triggers the &#8220;Open File&#8221; dialog event and place it on the page.<\/li>\n\n\n\n<li>Create a method to receive the file selection event and process it.<\/li>\n<\/ol>\n\n\n\n<p>In this application, I also created methods to handle the case where changes have been made to the subtitle text and prompt the user to either discard or keep those changes. This resulted in having two methods for handling each type of file (audio and text) separately, although they perform similar operations. In hindsight, it would be better to reuse code by making them more modular, but as it stands, there are separate codes for each. The process of reading and writing files is quite complex and requires a lot of attention to detail, so I found this part of the project to be the most challenging. From now on, I will explain the code in a step-by-step manner, following the actual workflow.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"218\"><code># Speech file picker control\nself.pick_speech_file_dialog = ft.FilePicker(on_result=self.pick_speech_file_result)<\/code><\/pre><\/div>\n\n\n\n<p>This is an instance of the <code>FilePicker<\/code> control, which opens the OS&#8217;s &#8220;Open File&#8221; dialog. When a file is actually selected, the <code>on_result<\/code> event occurs and calls the <code>self.pick_speech_file_result<\/code> method.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"809\"><code># Adds dialog instance methods to the page.\npage.overlay.extend([app.pick_speech_file_dialog, app.pick_text_file_dialog, \n                     app.export_as_srt_dialog, app.export_as_txt_dialog])<\/code><\/pre><\/div>\n\n\n\n<p>The dialog is added to the page using <code>overlay.extend<\/code>, which will be used for all file reading and writing operations. This is similar to adding an <code>Audio<\/code> control, and it&#8217;s being done outside of the class in the <code>async def main()<\/code> method.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"210\"><code># Open speech file button\nself.speech_file_button = ft.ElevatedButton(\n    text=&#39;Open Speech File&#39;, \n    icon=ft.icons.RECORD_VOICE_OVER_OUTLINED, \n    width=210,\n    on_click=self.pre_pick_speech_file,\n)<\/code><\/pre><\/div>\n\n\n\n<p>This is a button that calls <code>self.pre_pick_speech_file<\/code> when clicked.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"537\"><code># Called once Open Speech File button is clicked to pause playback and check if changes saved.\nasync def pre_pick_speech_file(self, e):\n    if self.isPlaying == True:\n        await self.play_button_clicked(e)\n    if self.save_button.text == &#39;*Save&#39;:\n        #print(&#39;Save is not done.&#39;)\n        await self.speech_save_or_cancel()\n    else:\n        await self.pick_speech_file()<\/code><\/pre><\/div>\n\n\n\n<p>This method is added to perform some processing before actually opening the &#8220;Open File&#8221; dialog. First, if playback is in progress, it stops. Then, if there are unsaved changes to the subtitles (indicated by an asterisk next to the &#8220;Save&#8221; button), a prompt dialogue is displayed to ask  moving forward without save, and only after that, the method for opening the &#8220;Open File&#8221; dialog is called. In this case, all method calls require <code>await<\/code>. To temporarily stop playback, <code>self.play_button_clicked(e)<\/code> is called with the argument <code>e<\/code> since it&#8217;s required even if it&#8217;s not being used. <\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"606\"><code># Opens a dialog if change is not saved.\nasync def speech_save_or_cancel(self):\n    self.page.dialog = self.speech_save_or_cancel_dialog\n    self.speech_save_or_cancel_dialog.open = True\n    self.page.update()<\/code><\/pre><\/div>\n\n\n\n<p>This method is called when there are unsaved changes. What&#8217;s being done are specifying an instance of AlertDialog (<code>self.speech_save_or_cancel_dialog<\/code>) as the dialog for the page, and setting its <code>open<\/code> property to enable displaying the dialog.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"224\"><code># Alert dialog that opens if subtitle was edited but not saved when Open Speech File button is clicked.\nself.speech_save_or_cancel_dialog = ft.AlertDialog(\n    modal=True,\n    title=ft.Text(&#39;Change not saved.&#39;),\n    content=ft.Text(&#39;Do you want to discard the change?&#39;),\n    actions=[\n         #ft.TextButton(&#39;Save&#39;, on_click=self.save_then_open, tooltip=&#39;Save then open another file.&#39;),\n         ft.TextButton(&#39;Open without save&#39;, on_click=self.open_speech_without_save, tooltip=&#39;Change will be lost.&#39;),\n         ft.TextButton(&#39;Cancel&#39;, on_click=self.close_speech_save_or_cancel_dialog),\n    ]\n)<\/code><\/pre><\/div>\n\n\n\n<p>This is a dialog that opens when there are unsaved changes. It has buttons for &#8220;Open without save&#8221; and &#8220;Cancel&#8221;, which allow you to open the file without saving or cancel the operation respectively. Although I wanted to add a button to save here as well, it didn&#8217;t work out and the &#8220;Save&#8221; button remains commented out.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"612\"><code># Closes the above dialog.\nasync def close_speech_save_or_cancel_dialog(self, e):\n    self.speech_save_or_cancel_dialog.open = False\n    self.page.update()<\/code><\/pre><\/div>\n\n\n\n<p>This is the cancel processing. It simply sets the <code>open<\/code> property of the dialog to <code>False<\/code>, closing it. <\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"617\"><code># Opens audio file pick.\nasync def open_speech_without_save(self, e):\n    self.speech_save_or_cancel_dialog.open = False\n    self.page.update()\n    await self.pick_speech_file()<\/code><\/pre><\/div>\n\n\n\n<p>This is a method called when the user selects to open the file without saving in the dialog. It closes the dialog, updates the page, and then calls <code>self.pick_speech_file()<\/code>.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"537\"><code># Opens audio file pick dialog. Only allow compatible extensions.\nasync def pick_speech_file(self):\n    self.pick_speech_file_dialog.pick_files(\n        dialog_title=&#39;Select a speech (audio) file&#39;,\n        allow_multiple=False,\n        allowed_extensions=[&#39;mp3&#39;, &#39;m4a&#39;, &#39;wav&#39;, &#39;mp4&#39;, &#39;aiff&#39;, &#39;aac&#39;],\n        file_type=ft.FilePickerFileType.CUSTOM,\n    )<\/code><\/pre><\/div>\n\n\n\n<p>Finally, this is the method for opening the &#8220;Open File&#8221; dialog. This method is used to limit the file types that can be opened by setting two properties: <code>allowed_extensions<\/code> and <code>file_type=ft.FilePickerFileType.CUSTOM<\/code>. This method opens the &#8220;Open File&#8221; dialog using the <code>pick_files()<\/code> method of the <code>self.pick_speech_file_dialog<\/code> control, which was previously defined. When a file is selected, the <code>on_result<\/code> event occurs and calls the <code>self.pick_speech_file_result<\/code> method. Since this method uses OS functionality, it does not require keeping track of the previously opened folder within the Flet app; when opening a file again, the same folder will be opened.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"546\"><code># Called when audio file pick dialog is closed. If file is selected, call self.check_text_file to load text file.\nasync def pick_speech_file_result(self, e: ft.FilePickerResultEvent):\n    if e.files:\n        #print(f&#39;e.files = {e.files}&#39;)\n        self.speech_file_name.value = &#39;&#39;.join(map(lambda f: f.name, e.files))\n        self.speech_file = &#39;&#39;.join(map(lambda f: f.path, e.files))\n        #print(f&#39;Full path= {self.speech_file}&#39;)\n        self.audio1.src = self.speech_file\n        self.base_dir.value=f&quot;Directory: {os.path.dirname(self.speech_file)}&quot;\n        await self.check_text_file()\n        self.update()\n        await self.load_audio()<\/code><\/pre><\/div>\n\n\n\n<p>This method takes an argument <code>e<\/code> in the <code>ft.FilePickerResultEvent<\/code>, which contains information about the opened file. It extracts the file name <code>f.name<\/code> and absolute path <code>f.path<\/code> from <code>e.files<\/code>. It assigns the file name and path to <code>self.speech_file_name.value<\/code> for display purposes and <code>self.audio1.src<\/code> for loading the audio file into <code>self.speech_file<\/code>. It then calls the <code>async self.check_text_file()<\/code> method, which checks if file exists, updates the display, and finally loads the audio file using the<br><code>load_audio()<\/code> function.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"589\"><code># Checks if audioFileName.srt or .txt exists to automatically load it.\nasync def check_text_file(self):\n    #print(f&#39;Speech file = {self.speech_file}&#39;)\n    tmp_file = os.path.splitext(self.speech_file)[0]\n    if os.path.exists(tmp_file+&#39;.srt&#39;):\n        self.text_file = tmp_file+&#39;.srt&#39;\n        self.text_file_name.value = os.path.basename(self.text_file)\n    elif os.path.exists(tmp_file+&#39;.txt&#39;):\n        self.text_file = tmp_file+&#39;.txt&#39;\n        self.text_file_name.value = os.path.basename(self.text_file)\n    else:\n        self.text_file = self.text_file_name.value = &#39;No Text File.&#39;\n        self.save_button.disabled=True\n        self.export_button.disabled=True\n        self.sub_scroller_sw.disabled=True\n    #print(f&#39;Subtitle file = {self.text_file_name.value}&#39;)<\/code><\/pre><\/div>\n\n\n\n<p>This method prepares to read the selected audio file, checking if a file with the same name but with an extension of .srt or .txt exists. If neither exists, it disables buttons such as Save.<\/p>\n\n\n\n<p>After this, the method <code>self.load_audio()<\/code> on line 801 is called, which adds the audio file to the page. When the loading of the audio file is complete, the event <code>on_loaded<\/code> is triggered for <code>self.audio1<\/code>, and then the method <code>self.loaded<\/code> explained at the beginning is called.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">There is no code to evaluate whether the file contents are correct, but the combination of codes is this length. Although understanding the process can make it less complex, going through it in one's head can be quite challenging. When adding code for reading subtitle files, I created a checklist on Smartsheet free version (example capture below) and made progress by coding accordingly. This article will not cover the part related to reading text files because it's doing pretty much the same thing.<\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"875\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-1024x875.jpg\" alt=\"\" class=\"wp-image-1246\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-1024x875.jpg 1024w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-300x256.jpg 300w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-768x656.jpg 768w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-1536x1312.jpg 1536w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4.jpg 1555w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">async def save_clicked(self, e), called to save and overwrite subtitle files<\/h4>\n\n\n\n<p>This method, located on lines 641-651, calls a method to overwrite the open subtitle file with the changed content. The call happens only when changes are made based on the open file type, .srt or .txt.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"640\"><code>    # Updates current open file.\n    async def save_clicked(self, e):\n        #print(f&#39;File: {self.text_file}&#39;)\n        extension = os.path.splitext(self.text_file)[1]\n        #print(f&#39;Extension: {extension}&#39;)\n        if self.save_button.text==(&#39;*Save&#39;):\n            if extension == &#39;.srt&#39;:\n                await self.save_as_srt(self.text_file)\n            elif extension == &#39;.txt&#39;:\n                await self.save_as_txt(self.text_file)\n            self.save_button.text=(&#39;Save&#39;)\n        self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Overwriting SRT file, async def save_as_srt(self, save_file_name)<\/h4>\n\n\n\n<p>This method, located on lines 670-684, overwrites an SRT file. The <code>save_file_name<\/code> variable contains the absolute path of the file that was opened. The <code>self.subtitles<\/code> list is formatted for ease of use within the app, so it writes to the file in the format of index number, start time &#8211;&gt; end time, and subtitle text with a following blank line (<code>\\n<\/code>). After writing is complete, it sends a notification message to the bottom of the window and updates the display.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"669\"><code>    # Saves as .srt file.\n    async def save_as_srt(self, save_file_name):\n        with open(save_file_name, &#39;w&#39;) as srt:\n            for i in self.subtitles:\n                for j in range(len(i)):\n                    if j % 4 == 0:\n                        srt.write(&#39;%sn&#39; % i[j])\n                    elif j % 4 == 1:\n                        start = ms_to_hhmmssnnn(int(i[j]))\n                        end = ms_to_hhmmssnnn(i[j+1])\n                        srt.write(f&#39;{start} --&gt; {end}n&#39;)\n                    elif j % 4 == 3:\n                        srt.write(&#39;%snn&#39; % i[j]) \n        notification = f&#39;Subtitle saved as an SRT file: {os.path.basename(save_file_name)}&#39;\n        await self.open_notification_bar(notification)\n        self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Overwriting TXT file, async def save_as_txt(self, save_file_name)<\/h4>\n\n\n\n<p>This method, located on lines 705-713, overwrites a TXT file. Unlike SRT files, which contain additional information such as timestamps and blank lines, the subtitles are simply represented as strings in this format. Therefore, it only writes the string parts of the <code>self.subtitles<\/code> list to the file. After writing is complete, it sends a message to the bottom of the window and updates the display.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"704\"><code>    # Saves as .txt file.\n    async def save_as_txt(self, save_file_name):\n        with open(save_file_name, &#39;w&#39;) as txt:\n            for i in self.subtitles:\n                for j in range(len(i)):\n                    if j % 4 == 3:\n                        txt.write(&#39;%sn&#39; % i[j]) \n        notification = f&#39;Subtitle saved as a TXT file: {os.path.basename(save_file_name)}&#39;\n        await self.open_notification_bar(notification)\n        self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Export as SRT\/TXT, async def export_as_srt(self, e) and async def export_as_txt(self, e)<\/h4>\n\n\n\n<p>The lines 654-667 for exporting as SRT and 687-702 for exporting as TXT could have been merged and simplified, but I was too lazy to do it. When either button is clicked, if a file with the same name already exists, it will suggest a new name by adding the date and time to the filename and open a file save dialog. When a TXT file is open, only export as TXT is enabled since there is no timestamp information (it&#8217;s unable to generate a SRT file).<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"333\" height=\"55\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-6.png\" alt=\"\" class=\"wp-image-1269\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-6.png 333w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-6-300x50.png 300w\" sizes=\"auto, (max-width: 333px) 100vw, 333px\" \/><figcaption class=\"wp-element-caption\">TXT cannot be exported as SRT.<\/figcaption><\/figure>\n\n\n\n<p>Similarly, when opening a dialog to allow the user to specify the file name and save location, separate code is needed for adding controls, pages, and processing. The flow is almost identical to audio file loading, so I won&#8217;t go into details. However, depending on which button was clicked, the final result is writing to a file using either the <code>save_as_srt<\/code> or <code>save_as_txt<\/code> method introduced earlier.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code to export as SRT:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"653\"><code># Exports current open SRT file as another SRT file.\nasync def export_as_srt(self, e):\n    if os.path.splitext(self.text_file)[1] == &#39;.srt&#39;:\n        suggested_file_name = os.path.basename(self.text_file).split(&#39;.&#39;, 1)[0]+&#39;_&#39;+datetime.now().strftime(&quot;%Y%m%d%H%M&quot;)+&#39;.srt&#39;\n    self.export_as_srt_dialog.save_file(\n        dialog_title=&#39;Export as an SRT file&#39;,\n        allowed_extensions=[&#39;srt&#39;],\n        file_name = suggested_file_name,\n        file_type=ft.FilePickerFileType.CUSTOM,\n    )\n\n# Checks result of Export as SRT File Picker and passes absolute path to self.save_as_srt if exists.\nasync def export_as_srt_result(self, e: ft.FilePicker.result):\n    if e.path:\n        await self.save_as_srt(e.path)<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code to export as TXT:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"686\"><code># Exports current open text file as a TXT file.\nasync def export_as_txt(self, e):\n    if os.path.exists(os.path.splitext(self.text_file)[0]+&#39;.txt&#39;):\n        suggested_file_name = os.path.basename(self.text_file).split(&#39;.&#39;, 1)[0]+&#39;_&#39;+datetime.now().strftime(&quot;%Y%m%d%H%M&quot;)+&#39;.txt&#39;\n    else:\n        suggested_file_name = os.path.basename(self.text_file).split(&#39;.&#39;, 1)[0]+&#39;.txt&#39;\n    self.export_as_txt_dialog.save_file(\n        dialog_title=&#39;Export as a TXT file&#39;,\n        allowed_extensions=[&#39;txt&#39;],\n        file_name = suggested_file_name,\n        file_type=ft.FilePickerFileType.CUSTOM,\n    )\n\n# Checks result of Export as TXT File Picker and passes absolute path to self.save_as_txt if exists.\nasync def export_as_txt_result(self, e: ft.FilePicker.result):\n    if e.path:\n        await self.save_as_txt(e.path)<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Notification at the bottom, async def open_notification_bar(self, notification, type=&#8217;normal&#8217;)<\/h4>\n\n\n\n<p>This method, located on lines 716-725, displays a notification at the bottom of the window. This feature utilizes the <code><a href=\"https:\/\/flet.dev\/docs\/controls\/snackbar\/\" target=\"_blank\" rel=\"noreferrer noopener\">SnackBar<\/a><\/code> control defined at line 370 in Flet, which is displayed only when needed and automatically disappears. Sample notification content:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"357\" height=\"57\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-5.png\" alt=\"\" class=\"wp-image-1265\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-5.png 357w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-5-300x48.png 300w\" sizes=\"auto, (max-width: 357px) 100vw, 357px\" \/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"339\" height=\"75\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4.png\" alt=\"\" class=\"wp-image-1264\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4.png 339w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-4-300x66.png 300w\" sizes=\"auto, (max-width: 339px) 100vw, 339px\" \/><\/figure>\n\n\n\n<p>This method displays a notification with the specified text and type (normal or error). If no type is specified, it defaults to a 2-second notification duration. For error notifications, it displays a  red notification on yellow backend with a longer duration of 4 seconds (4000 ms). There are various ways to specify colors, but I used <a href=\"https:\/\/flet-controls-gallery.fly.dev\/colors\/colorpalettes\">named colors from this page<\/a>. The text color is specified as a property of the Text control within the SnackBar&#8217;s <code>content<\/code> property, while the notification area color <code>bgcolor<\/code> is specified as a property of the SnackBar itself &#8211; it is not intuitive. After configuring the content, open the notification with <code>open=True<\/code>, and it disappears after the specified time automatically.<\/p>\n\n\n\n<p>Definition of the control is right below (line 369~), and the method starts from line 715.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"369\"><code>    # Notification bar control at the bottom\n    self.notification_bar=ft.SnackBar(\n        content=ft.Text(&#39;Speech + Subtitle Player&#39;),\n        duration=2000,\n        bgcolor=ft.colors.BLUE_GREY_700,\n    )<\/code><\/pre><\/div>\n\n\n\n<p>Upon reviewing it again, I realize that the control definition above already specifies the notification area color, so there shouldn&#8217;t be no need to specify it again in the method.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"715\"><code>    # Opens notification bar with given text. If type is &#39;error&#39;, shows message longer with caution color.\n    async def open_notification_bar(self, notification, type=&#39;normal&#39;):\n        if type == &#39;normal&#39;:\n            self.notification_bar.content=ft.Text(notification, color=ft.colors.LIGHT_BLUE_ACCENT_400)\n            self.notification_bar.bgcolor=ft.colors.BLUE_GREY_700\n        elif type == &#39;error&#39;:\n            self.notification_bar.content=ft.Text(notification, color=ft.colors.RED)\n            self.notification_bar.bgcolor=ft.colors.YELLOW\n            self.notification_bar.duration=4000\n        self.notification_bar.open=True \n        self.notification_bar.update()<\/code><\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Class to generate subtitle buttons, SubButton<\/h3>\n\n\n\n<p>The class that creates buttons from subtitles and timestamps is a custom control class that inherits from <code>UserControl<\/code> and implements user-defined controls. This class was created by modifying the official <a href=\"https:\/\/flet.dev\/docs\/tutorials\/python-todo\" target=\"_blank\" rel=\"noreferrer noopener\">Flet Tutorial&#8217;s &#8220;To-Do app&#8221; example<\/a>, so its internal structure is slightly different from the main class. It follows the order of initialization, layout building, button click method, etc.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Initialization method, def <strong>init<\/strong>(self, index, start_time, end_time, text, sub_time_clicked, play_button, save_button, subtitles)<\/h4>\n\n\n\n<p>When the parent class creates an instance of a button, it passes not only the index number, start time, and text of the subtitle related to display, but also methods of the parent class and the save button. The <code>subtitles<\/code> list, which holds the loaded subtitles as a 2D list, is passed so that it can be directly manipulated when editing the text. Passing the entire subtitles to each instance is not a good example to follow &#8211; it can be the reason of laggy window movement. <\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"83\"><code>def __init__(self, index, start_time, end_time, text, sub_time_clicked, play_button, save_button, subtitles):\n    super().__init__()\n    # Parameter of each subtitle.\n    self.index = index\n    self.start_time = start_time\n    self.end_time = end_time\n    self.text = text\n    # Passed methods and controls to call and update.\n    self.sub_time_clicked = sub_time_clicked\n    self.play_button = play_button\n    self.save_button = save_button\n    self.subtitles = subtitles<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Layout of time stamp and subtitle buttons, etc.,  def build(self)<\/h4>\n\n\n\n<p>In lines 97-150, the control instance is generated and initialized, and then returned to the parent class. The first half of the code up to line 123 creates buttons for timestamps, subtitles, and a placeholder for editing mode, and then wraps them together into the display control <code>self.display_view<\/code>.<\/p>\n\n\n\n<p>The key of <code>self.display_start_time<\/code> is the index number, which serves as a target specification for scrolling when a timestamp button is clicked. <\/p>\n\n\n\n<p>The <code>if<\/code> block starting from line 126 checks the type of subtitle file loaded and modifies the tooltip displayed when hovering over the timestamp button.<\/p>\n\n\n\n<p>Lines 132-149 are setting up the editing mode for subtitle text. By default, it is set to <code>visible=False<\/code>, making it invisible. <\/p>\n\n\n\n<p>The official To-Do app example has edit and delete buttons for each item, but in this application, the delete button is not necessary. Instead, the edit button is replaced with a subtitle itself that serves as a button, making it more intuitive to use. Additionally, by allowing the editing mode to be cancelled, I avoided implementing an undo feature.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"733\" height=\"144\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-9.png\" alt=\"\" class=\"wp-image-1393\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-9.png 733w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-9-300x59.png 300w\" sizes=\"auto, (max-width: 733px) 100vw, 733px\" \/><figcaption class=\"wp-element-caption\">Clicking on subtitle button enables editing. Enter key to settle.<\/figcaption><\/figure>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"96\"><code># === BUILD METHOD ===\ndef build(self):\n    # Start time button\n    self.display_start_time = ft.TextButton(text=f&quot;{ms_to_hhmmssnnn(int(self.start_time))}&quot;,\n                                        # Disable jump button if loaded text is TXT, no timestamp.\n                                        disabled=(self.start_time==201355555),\n                                        # When enabled, jump to the key when clicked.\n                                        key=self.index,\n                                        width=130,\n                                        on_click=self.jump_clicked,)\n\n    # Subtitle text button in display view. Click to edit.\n    self.display_text= ft.TextButton(text=f&quot;{self.text}&quot;, \n                                     on_click=self.edit_clicked, \n                                     tooltip=&#39;Click to edit&#39;)\n\n    # Placeholder of subtitle text button in edit view.\n    self.edit_text = ft.TextField(expand=1)\n\n    # Put controls together. Left item is the key=index.\n    self.display_view = ft.Row(\n        alignment=ft.MainAxisAlignment.START,\n        controls=[\n            ft.Text(value=self.index, width=30),\n            self.display_start_time,\n            self.display_text,\n        ]\n    )\n\n    # Change tool tip of start time button which is only clickable for SRT.\n    if self.start_time==201355555:\n        self.display_start_time.tooltip=&#39;Jump not available&#39;\n    else:\n        self.display_start_time.tooltip=&#39;Click to jump here&#39;\n\n    # Subtitle edit view visible when clicked.\n    self.edit_view = ft.Row(\n        visible=False,\n        #alignment=ft.MainAxisAlignment.SPACE_BETWEEN,\n        #vertical_alignment=ft.CrossAxisAlignment.CENTER,\n        controls=[\n            self.edit_text,\n            ft.IconButton(\n                icon=ft.icons.DONE_OUTLINE_OUTLINED,\n                tooltip=&#39;Update Text&#39;,\n                on_click=self.save_clicked,\n            ),\n            ft.IconButton(\n                icon=ft.icons.CANCEL_OUTLINED,\n                tooltip=&#39;Close wihout change&#39;,\n                on_click=self.cancel_clicked,\n            )\n        ]\n    )\n    return ft.Column(controls=[self.display_view, self.edit_view])<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Subtitle editing mode, async def edit_clicked(self, e)<\/h4>\n\n\n\n<p>Lines 155-161 enable the editing mode when a subtitle button is clicked. By calling the <code>focus()<\/code> method, it immediately allows keyboard input to be made, and sets up the <code>on_submit<\/code> event to call the <code>self.save_clicked<\/code> method when the Enter key is pressed.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"154\"><code># Opens editable text button with subtitle. Hit enter key or click checkmark to call save_clicked.\nasync def edit_clicked(self, e):\n    self.edit_text.value = self.display_text.text\n    self.edit_text.focus()\n    self.display_view.visible = False\n    self.edit_view.visible = True\n    self.edit_text.on_submit = self.save_clicked\n    self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Update of subtitle, async def save_clicked(self, e)<\/h4>\n\n\n\n<p>Lines 164-172 handle the processing when a check (overwrite) button is clicked or when Enter is pressed, settling the edited subtitle. The save button will have an asterisk (*) indicating that it&#8217;s been edited, and the <code>self.subtitles<\/code> list, which holds the subtitles, is updated with the changed text. Focus is returned to the audio file playback button, allowing for playback and pause control via Space or Enter keys.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-python\" data-lang=\"Python\" data-start=\"163\"><code># Updates edited subtitle, change save button, revert focus back to Play button.\nasync def save_clicked(self, e):\n    self.display_text.text= self.edit_text.value\n    self.display_view.visible = True\n    self.edit_view.visible = False\n    self.save_button.text = &#39;*Save&#39;\n    self.subtitles[int(self.index)-1][3]=self.display_text.text\n    self.play_button.focus()\n    self.save_button.update()\n    self.update()<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Cancel editing, async def cancel_clicked(self, e)<\/h4>\n\n\n\n<p>Lines 175-179 handle the editing cancellation processing when the (\u00d7) button is clicked. Although it&#8217;s called &#8220;cancel&#8221;, it simply discards the changed content and ends the editing mode, returning focus to the playback button. Unfortunately, I had wanted to achieve this same behavior using the Esc key, but unfortunately, Flet didn&#8217;t have a simple way to do so, so I had to give up on that idea.<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary>See the code:<\/summary>\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\" data-start=\"181\"><code># When timestamp clicked calls AudioSubPlayer.sub_time_clicked to jump to button position.\nasync def jump_clicked(self, e):\n    await self.sub_time_clicked(self.start_time)<\/code><\/pre><\/div>\n<\/details>\n\n\n\n<h4 class=\"wp-block-heading\">Jump by timestamp button,  async def jump_clicked(self, e)<\/h4>\n\n\n\n<p>Lines 182-183 pass the <code>start_time<\/code> of the subtitle to the parent class&#8217;s <code>sub_time_clicked<\/code> method when a timestamp button is clicked, setting up playback from that point. This is necessary to use <code>await<\/code> because we need to use the <code>seek<\/code> method of the Audio control, which requires an asynchronous operation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Functions not related to Flet GUI<\/h3>\n\n\n\n<p>Functions that are necessary for the app, but not related to Flet GUI, have been grouped together at the top of the code. Brief explanation of them are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A function converts milliseconds to a time string in SRT format<\/li>\n\n\n\n<li>A function does the reverse conversion<\/li>\n\n\n\n<li>A function reads and processes subtitle files (TXT or SRT) into a list that can be used within the app<\/li>\n<\/ul>\n\n\n\n<p>This is all of the explanation of the code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Things I Thought About While Creating This App and Why I Wrote This Article<\/h2>\n\n\n\n<p>Flet is really easy to create a modern design app, and that&#8217;s its big charm. Of course, buttons and text that become GUI elements are built-in, as well as audio file processing, sliders, notifications, and dialogs, so you can just roughly arrange them without worrying about the details and create an app quickly. I feel it&#8217;s like having a high-quality 3D printer, where Python code becomes real, just like 3D model data turns into a physical object. That&#8217;s exciting.<\/p>\n\n\n\n<p>The official documentation is comprehensive and can be well understood, allowing you to use it for your app. Since most of the features are designed to work in a web browser, there are many live examples available that you can try out. It&#8217;s also enjoyable to explore and find components to use in your own app by modifying them. By going to <a href=\"https:\/\/flet-controls-gallery.fly.dev\/layout\" target=\"_blank\" rel=\"noreferrer noopener\">this gallery page<\/a>, you can likely test most of the controls and features, and even check the actual code on GitHub, which is very helpful.<\/p>\n\n\n\n<p>However, Flet is not a mouse-operated GUI creation tool like those that were popular in the distant past. Therefore, all aspects of your app will need to be implemented using Python code. Additionally, the completed app&#8217;s interface will be implemented as a web frontend (HTML, JavaScript, CSS). While knowledge of these is not essential, having some understanding of them can make creating a finished product easier. I imagine that someone who has no intention of learning about front-end development might still be able to use Flet effectively by reading 1-2 introductory web front-end books and then understanding the documentation with ease.<\/p>\n\n\n\n<p>It seems that there is an increasing amount of Japanese information about Flet, but when I searched for articles of Flet, most of them were just &#8220;trying it out&#8221; type of articles. I couldn&#8217;t find many articles that actually explained how to create a practical app using Flet. While the official documentation covers everything, I thought it was still not simple enough when you actually use it.<\/p>\n\n\n\n<p>So, at the time when my app &#8220;Speech plus Subtitles Player&#8221;, was almost complete, I decided to write an article about developing an Flet app. My intention was that there must be people who really need such an article, even if it&#8217;s imperfect and has some unnecessary parts. I&#8217;m glad finally both the app and this document are done.<\/p>\n\n\n\n<p>For the Python logic parts, I relied heavily on Copilot (free version) to help me write them. It appears that Copilot doesn&#8217;t know much about Flet itself, but in terms of Python, I found that Copilot was quite reliable. The Large Language Model (LLM, AI) has made it possible to use code generation tools that can even do debugging together with them. This means that if you have an idea, the difficulty of creating an app has significantly decreased. (by the way, I got a huge help from LLMs for translation of this article from Japanese into English. Last half was almost done by <a href=\"https:\/\/ollama.com\/library\/llama3:8b-instruct-q8_0\" target=\"_blank\" rel=\"noreferrer noopener\">Llama 3 8B Instruct with Ollama<\/a>.)<\/p>\n\n\n\n<p>It&#8217;s a great feeling to see your own idea come to life, even if it&#8217;s a simple app. Once built, you can share it with others and get feedback. I highly recommend trying out Flet for creating apps &#8211; it&#8217;s definitely worth a shot!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Image by Stable Diffusion<\/h2>\n\n\n\n<p>It seems to be caused by the model I used, and most of the generated images were of realistic white old men. I replaced the prompt &#8220;realistic, masterpiece, best quality&#8221; with &#8220;cartoon&#8221;. From among the drawings I received, I adopted the one that mostly ignored my instructions as the eye-catching image for this article. The model remains the same, but the difference in generated image is significant due to changes in prompts.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"972\" height=\"739\" src=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-8.jpg\" alt=\"\" class=\"wp-image-1285\" srcset=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-8.jpg 972w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-8-300x228.jpg 300w, https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/image-8-768x584.jpg 768w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/figure>\n\n\n\n<p>Date:<br>2024-4-8 19:16:06<\/p>\n\n\n\n<p>Model:<br>realisticVision-v20_split-einsum<\/p>\n\n\n\n<p>Size:<br>512 x 512<\/p>\n\n\n\n<p>Include in Image:<br>cartoon, retro future, guy partially gray hair with glasses, white t-shirt, typing keyboard, happy coding!<\/p>\n\n\n\n<p>Exclude from Image:<br>frame, old, fat, suit<\/p>\n\n\n\n<p>Seed:<br>2776787021<\/p>\n\n\n\n<p>Steps:<br>50<\/p>\n\n\n\n<p>Guidance Scale:<br>20.0<\/p>\n\n\n\n<p>Scheduler:<br>DPM-Solver++<\/p>\n\n\n\n<p>ML Compute Unit:<br>CPU &amp; Neural Engine<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Flet can let you develop cool desktop apps in Python. I previously released an app that could play audio and d &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python.&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":1123,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_locale":"en_US","_original_post":"https:\/\/blog.peddals.com\/?p=1105","footnotes":""},"categories":[24,8,4],"tags":[12,18],"class_list":["post-1302","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-flet","category-macos","category-python","tag-mac","tag-python","en-US"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.4 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How I developed &quot;Speech + Subtitles Player&quot; desktop app with Flet for Python. | Peddals Blog<\/title>\n<meta name=\"description\" content=\"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How I developed &quot;Speech + Subtitles Player&quot; desktop app with Flet for Python. | Peddals Blog\" \/>\n<meta property=\"og:description\" content=\"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f\" \/>\n<meta property=\"og:url\" content=\"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/\" \/>\n<meta property=\"og:site_name\" content=\"Peddals Blog\" \/>\n<meta property=\"article:published_time\" content=\"2024-04-29T15:08:51+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-05-26T16:01:08+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png\" \/>\n\t<meta property=\"og:image:width\" content=\"512\" \/>\n\t<meta property=\"og:image:height\" content=\"512\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Handsome\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Handsome\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"163 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/\"},\"author\":{\"name\":\"Handsome\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#\\\/schema\\\/person\\\/81b2dabca748c3d11a45722f02d9d994\"},\"headline\":\"How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python.\",\"datePublished\":\"2024-04-29T15:08:51+00:00\",\"dateModified\":\"2024-05-26T16:01:08+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/\"},\"wordCount\":7271,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#\\\/schema\\\/person\\\/81b2dabca748c3d11a45722f02d9d994\"},\"image\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/blog.peddals.com\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png\",\"keywords\":[\"mac\",\"Python\"],\"articleSection\":[\"Flet\",\"macOS\",\"Python\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/\",\"url\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/\",\"name\":\"How I developed \\\"Speech + Subtitles Player\\\" desktop app with Flet for Python. | Peddals Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/blog.peddals.com\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png\",\"datePublished\":\"2024-04-29T15:08:51+00:00\",\"dateModified\":\"2024-05-26T16:01:08+00:00\",\"description\":\"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#primaryimage\",\"url\":\"https:\\\/\\\/blog.peddals.com\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png\",\"contentUrl\":\"https:\\\/\\\/blog.peddals.com\\\/wp-content\\\/uploads\\\/2024\\\/04\\\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png\",\"width\":512,\"height\":512},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/en\\\/how-i-developed-speech-plus-subtitle-player-flet-app\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"\u30db\u30fc\u30e0\",\"item\":\"https:\\\/\\\/blog.peddals.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python.\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#website\",\"url\":\"https:\\\/\\\/blog.peddals.com\\\/\",\"name\":\"Peddals Blog\",\"description\":\"AI, LLM, Python, Mac, Pythonista3, iOS, etc. in Japanese and English\",\"publisher\":{\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#\\\/schema\\\/person\\\/81b2dabca748c3d11a45722f02d9d994\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/blog.peddals.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/blog.peddals.com\\\/#\\\/schema\\\/person\\\/81b2dabca748c3d11a45722f02d9d994\",\"name\":\"Handsome\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g\",\"caption\":\"Handsome\"},\"logo\":{\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How I developed \"Speech + Subtitles Player\" desktop app with Flet for Python. | Peddals Blog","description":"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/","og_locale":"en_US","og_type":"article","og_title":"How I developed \"Speech + Subtitles Player\" desktop app with Flet for Python. | Peddals Blog","og_description":"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f","og_url":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/","og_site_name":"Peddals Blog","article_published_time":"2024-04-29T15:08:51+00:00","article_modified_time":"2024-05-26T16:01:08+00:00","og_image":[{"width":512,"height":512,"url":"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png","type":"image\/png"}],"author":"Handsome","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Handsome","Est. reading time":"163 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#article","isPartOf":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/"},"author":{"name":"Handsome","@id":"https:\/\/blog.peddals.com\/#\/schema\/person\/81b2dabca748c3d11a45722f02d9d994"},"headline":"How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python.","datePublished":"2024-04-29T15:08:51+00:00","dateModified":"2024-05-26T16:01:08+00:00","mainEntityOfPage":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/"},"wordCount":7271,"commentCount":0,"publisher":{"@id":"https:\/\/blog.peddals.com\/#\/schema\/person\/81b2dabca748c3d11a45722f02d9d994"},"image":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#primaryimage"},"thumbnailUrl":"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png","keywords":["mac","Python"],"articleSection":["Flet","macOS","Python"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/","url":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/","name":"How I developed \"Speech + Subtitles Player\" desktop app with Flet for Python. | Peddals Blog","isPartOf":{"@id":"https:\/\/blog.peddals.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#primaryimage"},"image":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#primaryimage"},"thumbnailUrl":"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png","datePublished":"2024-04-29T15:08:51+00:00","dateModified":"2024-05-26T16:01:08+00:00","description":"\u4ee5\u524d\u516c\u958b\u3057\u305f\u3001\u97f3\u58f0\u3068\u5b57\u5e55 \u306e\u540c\u6642\u518d\u751f\u30fb\u7de8\u96c6\u30a2\u30d7\u30ea\u306e\u4f5c\u308a\u65b9\u3092\u516c\u958b\u3057\u307e\u3059\u3002Python\u3067\u30c7\u30b9\u30af\u30c8\u30c3\u30d7\u30a2\u30d7\u30ea\u304c\u4f5c\u308c\u308bFlet\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f","breadcrumb":{"@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#primaryimage","url":"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png","contentUrl":"https:\/\/blog.peddals.com\/wp-content\/uploads\/2024\/04\/cartoon-retro-future-guy-partially-gray-hair-with-glasses-white-t-s.463.2776787021.png","width":512,"height":512},{"@type":"BreadcrumbList","@id":"https:\/\/blog.peddals.com\/en\/how-i-developed-speech-plus-subtitle-player-flet-app\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"\u30db\u30fc\u30e0","item":"https:\/\/blog.peddals.com\/"},{"@type":"ListItem","position":2,"name":"How I developed &#8220;Speech + Subtitles Player&#8221; desktop app with Flet for Python."}]},{"@type":"WebSite","@id":"https:\/\/blog.peddals.com\/#website","url":"https:\/\/blog.peddals.com\/","name":"Peddals Blog","description":"AI, LLM, Python, Mac, Pythonista3, iOS, etc. in Japanese and English","publisher":{"@id":"https:\/\/blog.peddals.com\/#\/schema\/person\/81b2dabca748c3d11a45722f02d9d994"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/blog.peddals.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/blog.peddals.com\/#\/schema\/person\/81b2dabca748c3d11a45722f02d9d994","name":"Handsome","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g","caption":"Handsome"},"logo":{"@id":"https:\/\/secure.gravatar.com\/avatar\/51d7363349ec538c4d62c9ebe89488fd7388729ad0c9dfeebd8bb32ebfb11f17?s=96&d=mm&r=g"}}]}},"_links":{"self":[{"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/posts\/1302","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/comments?post=1302"}],"version-history":[{"count":84,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/posts\/1302\/revisions"}],"predecessor-version":[{"id":1471,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/posts\/1302\/revisions\/1471"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/media\/1123"}],"wp:attachment":[{"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/media?parent=1302"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/categories?post=1302"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.peddals.com\/wp-json\/wp\/v2\/tags?post=1302"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}