Skip to main content

counting...

When you have...

Permissions 0644 for '~/.ssh/key.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.

Quick Fix

  • Command this for individual keys

    sudo chmod 600 ~/.ssh/key.pem

  • Command this for the SSH Key folder

    sudo chmod 700 ~/.ssh

So what are these random digits?

  • Each digit represents the access privilege of User, Group, and Other.

    7: 4(r) + 2(w) + 1(x) rwx read, write and execute 6: 4(r) + 2(w) rw- read and write 5: 4(r) + 1(x) r-x read and execute 4: 4(r) r-- read only 3: 2(w) + 1(x) -wx write and execute 2: 2(w) -w- write only 1: 1(x) --x execute only 0: 0 --- none

  • Therefore, chmod 600 means giving read and write access to the user and nothing to any other parties.

  • Giving 755 means giving full access to the user and read, execute access to any other parties.

  • Giving 777 🎰 means giving full access to everyone.

Note that Linux SSH manual says:

  • ~/.ssh/: This directory is the default location for all user-specific configuration and authentication information. There is no general requirement to keep the entire contents of this directory secret, but the recommended permissions are read/write/execute for the user and not accessible by others. (Recommends 700)
  • ~/.ssh/id_rsa: Contains the private key for authentication. These files contain sensitive data and should be readable by the user but not accessible by others (read/write/execute). ssh will simply ignore a private key file if it is accessible by others. It is possible to specify a passphrase when generating the key, which will be used to encrypt the sensitive part of this file using 3DES. (Recommends 600)

counting...

Disclaimer

  • Both the United States and the Republic of Korea allow limited usage of copyrighted material for educational use.

Notwithstanding the provisions of sections 17 U.S.C. § 106 and 17 U.S.C. § 106A, the fair use of a copyrighted work, including such use by reproduction in copies or phonorecords or by any other means specified by that section, for purposes such as criticism, comment, news reporting, teaching (including multiple copies for classroom use), scholarship, or research, is not an infringement of copyright.

  • For most countries, downloading recorded videos for educational purposes only, for such use where you might want to watch them under an unstable internet connection, would not entail legal troubles.
  • However, you understand that you use this code block at your own risk and would not use it in such a way that will jeopardize academic integrity or infringe any intellectual rights.

Usage

// This code is under MIT License.

let video = document.querySelector('video').src
let download = document.createElement('a')
let button = document.createElement('button')
button.innerText =
'To Download Video: Right Click Here → Save Link As'
download.append(button)
download.href = video
download.setAttribute('download', video)

document
.getElementsByClassName('transcript')[0]
.prepend(download)
  • Access the Zoom video recording page.
  • After the webpage completes loading—when you can both play the video and scroll through the chat list—open the browser console.
  • Paste this code and close the console.
  • There will be a random button on top of the chat list. Don't click it; right-click it and select Save Link As.
  • Now the video will download.

The backstory of reporting this to Zoom

In March of 2021, I have reported this to Zoom as I considered this a security matter. While anyone can technically record their screen to obtain a copy of the video, I thought the implications were different: when you can one-click to download the full video, and when it takes hours of effort to record the video and audio manually.

Furthermore, instructors can decide if they want to open up downloading the original copies. Therefore, this feature's whole purpose is to provide inconvenience to deter users from downloading files. In that sense, this code is a security bypass of that policy.

That's what I told Zoom HQ. They responded:

Thank you for your report. We have reproduced the behavior you have reported. However, while this UI does not expose the download URL for recordings which have opted to disable the download functionality, a user may still record the meeting locally using a screen-recording program. In addition, for the browser to be able to play the recording, it must be transmitted to the browser in some form, which an attacker may save during transmission, and so the prevention of this is non-trivial. We appreciate your suggestion and may look into making this change in the future, but at the moment, we consider this to be a Defense-In-Depth measure. With every fix, we must carefully weigh the usability tradeoffs of any additional security control. We are reasonably satisfied with our security at this time, and we have chosen not to make any changes to our platform for the time being. We will be closing this report, but we still want to thank you for all your effort in bringing this behavior to our attention. Thank you for thinking of Zoom security.

Well... It seems like they're not interested, and no patch will come soon. So, for the time being, use this code wisely, and abide by your laws!

counting...

Prerequisite

Final Goal

  • Press Left Command to set Mac's input method to English.
  • Press Right Command to set Mac's input method to Korean.
  • Any other shortcut combinations would perform as usual.

Instructions

  • Go to ~/.config/karabiner/assets/complex_modifications. You can click Command+Shift+G within Finder to open a goto window.
  • Create a JSON file like the following here (open any text editor and save it as filename.json).
  • Go to Karabiner-Elements.app → Complex Modifications and press add rules.
  • Click the rules that you want to enable. The above text file will show under Multilingual Input Methods.
{
"title": "Multilingual Input Methods",
"rules": [
{
"description": "R Command to 한글",
"manipulators": [
{
"type": "basic",
"from": {
"key_code": "right_command",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "key_code": "right_command", "lazy": true }
],
"to_if_alone": [
{
"select_input_source": { "language": "^ko$" }
}
]
}
]
},
{
"description": "L Command to English",
"manipulators": [
{
"type": "basic",
"from": {
"key_code": "left_command",
"modifiers": { "optional": ["any"] }
},
"to": [
{ "key_code": "left_command", "lazy": true }
],
"to_if_alone": [
{
"select_input_source": { "language": "^en$" }
}
]
}
]
}
]
}

Configuring more languages

Update Mar 7th, 2022

  • If you're bilingual, try looking into Eikana.
  • I switched to Eikana + Gureum configuration; there's a notorious 자소 분리 bug in macOS Korean Keyboard.

counting...

Expensive Jobs?

Notably:

  • Doctors 🧑‍⚕️
  • Lawyers 🧑‍⚖️

Why are expensive jobs expensive 💰?

Usually, an expensive labor value takes place when the demand for the work is very high, whereas the supply cannot increase. Moreover, health and legal issues regularly appear throughout our society (either injured or in a legal dispute), and it is doubtful for an individual to not benefit from either. In other words, these demands never vanish.

However, the supply always stagnates. Why?

  • Supply velocity remains slow. Because:
    • Skillful doctors and lawyers need extended training.
    • These "prestigious" institutions can only output dozens of jobs per year.
  • But we cannot pump out more supply. Why?
    • We lack secondary sources for such collection (i.e., people who can study for ten years are not abundant).
    • A forceful increase will entail societal backlash (i.e., unprofessional workers)
  • Such scarcely created suppliers work very slow
    • How many patients can a doctor see in a day?
      • There are complaints about Factory-style medical facilities, like conveyor belts.
    • How long does it take for a single lawsuit to resolve?

In the end, supply always falls behind demand.

Then why can AI replace expensive jobs?

Two aspects ① Economic Efficiency ② Performance.

Economic Efficiency

Making AI is expensive because:

  1. High-quality AI requires a high-purity dataset.
  2. For such high purity data, you need a lot of patternable data.

Making an AI is also tricky regardless of the field. To slightly exaggerate, creating a cleaning AI is as hard as making a medical AI.

  • To create a perfect cleaning AI...
    • To determine the pollution level of a room, you will need
      • millions of room photos and data matching the pollution level.
      • millions of datasets containing each type of pollution and its corresponding solutions are required.
        • Water spill. → Dishcloth
        • Garbage → Trash can
        • Dust → Vacuum
    • Each methodology needs to be thoroughly trained.
      • Analyze and train millions of behaviors cleaning with a dishcloth
      • Analyze and train millions of behaviors that contain and dispose of garbage
      • Analyze and train millions of behaviors that utilize vacuum cleaners very well

As a result, cleaning artificial intelligence also costs a lot of money. In other words, if it will be challenging to produce artificial intelligence anyways, you want a model that brings sufficient economic effects and versatile adaptability. Therefore, it is appropriate to train artificial intelligence for expensive labor to show this high financial return on investment level.

Performance

On the other hand, AI never forgets, and it can duplicate itself. Imagine:

  • A doctor who never fails his medical knowledge. A lawyer who remembers every case perfectly.
  • Cloning the best doctors and lawyers in class into thousands of AI, taking thousands of clients at once.
  • Instantly sharing newly discovered data.
  • Remembering every detail of the client and proactively preventing accidents.
  • Meeting my family doctor whenever, wherever.

Industry Resistance

SEOUL (Reuters) - South Korea's parliament on late Friday passed a controversial bill to limit ride-hailing service Tada, dealing a blow to a company that has been a smash hit since its launch in late 2018 but faced a backlash from taxi drivers angry over new mobility services. - South Korea passes bill limiting Softbank-backed ride-hailing service Tada | Reuters

Recent TADA Warfare exhibited a classic Alliance-versus-Megacorporation style of conflict. Taxi drivers eventually won, but it was a victory without victory --- since the winner was another conglomerate Kakao Mobility which finally took over the market.

Physicians and lawyers also show strong industry resistance. However, they also possess immense social power; one can easily imagine such scenarios:

Scenarios

  • Medical AI kills its patient! Can we bet our lives on such a lacking machine?
    • Regardless of the context, such social fear can lead to Tech Luddites.
  • Lawyer AI deemed discriminating? Can we let such biased agents take over our nation?
    • The bias of precedents can appear depending on how statistics are captured. If you maliciously capture statistics and frame specific vested Research as biased, it can spread to artificial intelligence distrust and rejection movements regardless of the context.

Potential Strategy

🐵

In the animal kingdom, there was a naive monkey. One day, a badger came and presented colorful sneakers to a monkey. The monkey didn't need shoes but received them as a gift. After that, badgers continued offering sneakers, and the callus on the monkey's feet gradually thinned. Soon, the monkey, unable to go out without shoes, became dependent on the badger.

Start with a platform system that helps doctors and lawyers.

  • DOCTORS: Start with Medical CRM. When a patient comes, information about the patient is collected before treatment begins. When treatment begins, the patient's story is automatically parsed, and artificial intelligence extracts keywords. Medical personnel verifies this. Similar cases and recommended care/prescriptions appear on one side of the screen. The doctor selects the appropriate treatment among the recommended treatments and proceeds with the treatment. Or a doctor can add a new therapy. This information is recorded on the server and used for extensive data training.
  • LAWYERS: Start with a case analyzer. It begins with a local legal case (e.g., traffic ticket violation), like DoNotPay - The World's First Robot Lawyer. However, after gradually increasing the number of issues that become databases, lawyers can search for similar topics like "Google Search." For example, if a fraud case comes in, the lawyer enters the details of the case. With dozens of previous precedents, artificial intelligence analyzes the similarities and differences of events.
  • Like GitHub Copilot but for medical and legal cases.

Like these, provide sneakers --- very essential and valuable tools for medical personnel and legal professionals. In other words, transform doctors and lawyers into our primary customers and data pipeline. When entering a robust market like the medical and legal circles, never engage in an all-out war. Instead, build cooperative relationships first, neutralize them, and then wage a full-scale war.

counting...

OK — I admit. The title is slightly misleading. You are reading a technical post about converting any video into an ASCII Art text stream that one can play on the terminal. The text stream here is a subtitle file. You can use any video player or terminal program to parse and display subtitles to play the music video. But the playing part is out of the scope of this post. Still don't get it? Here's a demo:

Enable subtitles and wait for a couple of seconds. If the video errors out, check out the following screen recording:

My text streams use braille to represent pixels. And to display consecutive streams of texts paired with music playback, what would be more suitable than the subtitle format? Therefore, I aim to convert any video into a YouTube subtitle. The tech stack is:

  • OpenCV (C++ cv2) — used to convert video into frames
  • Python Image Library (Python 3 Pillow) — used to convert frames into ASCII art (braille)
  • Python Standard Library (sys, os, pathlib) — used to read and write files
  • ffmpeg (optional) — used to pack everything into a video

Open-sourced on GitHub: anaclumos/video-in-dots.

note

Technically, braille characters are not ASCII characters. They are Unicode, but let's not be too pedantic.


Design

We need to first prove the concept (PoC) that the following technologies achieve our goal:

  1. Converting any image into a monochrome image
  2. Converting any monochrome image into ASCII art
  3. Converting any video into a series of images
  4. Converting any frames into a series of ASCII art and then packaging them into a subtitle file.
  5. (Figured out later) Compressing the subtitle files under a specific size.
  6. (Figured out later) Dithering the images to improve the quality of the ASCII art.

1. Converting any image into a monochrome image

A monochrome image is an image with 1-bit depth, comprised of #000000 and #FFFFFF colors. Note that grayscale images are not monochrome images. Grayscale images also have a wide range of gray colors between #000000 and #FFFFFF. We can use these pure black and white colors to represent the raised and lowered dots of the braille characters, to visually distinguish borders and shapes. Therefore, we convert an image into a BW image and again convert that into a 1-bit depth image. One detail we should note is that subtitles are usually white, so we want the white pixel in the monochrome image to represent 1, the raised dot in braille.

As you can see in the right three images, you can represent any image with border and shape with pure black and white. DemonDeLuxe (Dominique Toussaint), CC BY-SA 3.0, via Wikimedia Commons.

As you can see in the right three images, you can represent any image with border and shape with pure black and white. DemonDeLuxe (Dominique Toussaint), CC BY-SA 3.0, via Wikimedia Commons.

The leftmost image has 256 shades of gray, and the right three images have only two shades of gray, represented in different monochrome conversion algorithms. I used the Floyd-Steinberg dithering algorithm in this project.

Converting the image

There are many ways to convert an image into a monochrome image. However, this project only uses sRGB color space, so I used the CIE 1931 sRGB Luminance conversion algorithm. Wikipedia. Sounds fancy, but it's just a formula:

def grayscale(red: int, green: int, blue: int) -> int:
return int(0.2126 * red + 0.7152 * green + 0.0722 * blue)

red, green, and blue are the RGB values of the pixel, represented in integers from 0 to 255. If their sum goes over the hex_threshold, the pixel is white (1); otherwise, it is black. We can now run this code for every pixel. This grayscale code is for understanding the fundamentals. We will use Python PIL's convert function to convert the image into a monochrome image. This library also applies the Floyd-Steinberg dithering algorithm to the image.

resized_image_bw = resized_image.convert("1")  # apply dithering

2. Converting any monochrome image into arbitrary-sized ASCII arts

The above sentence has three parts. Let's break them down.

  1. Converting any monochrome image into
  2. Arbitrary-sized
  3. ASCII arts

We figured out the first, so now let's explore the second.

Resizing images with PIL

We can use the following code to resize an image in PIL:

def resize(image: Image.Image, width: int, height: int) -> Image.Image:
if height == 0:
height = int(im.height / im.width * width)
if height % braille_config.height != 0:
height = int(braille_config.height * (height // braille_config.height))
if width % braille_config.width != 0:
width = int(braille_config.width * (width // braille_config.width))
return image.resize((width, height))

I will use two-by-three braille characters, so I should slightly modify the height and width of the image to make it divisible by 2 and 3.

Converting the image

Seeing the image will help you better understand. For example, let's say we have the left image (6 by 6). We would cut the image into two-by-three pieces and converted each piece into a braille character.

Left → Right

Left → Right

The key here is to find the correct braille character to represent the two-by-three piece. A straightforward approach is to map all the two-by-three pieces into an array, especially since two-by-three braille characters only have 64 different combinations. But we can do better by understanding how Unicode assigns the character codes.

Note: Braille Patterns from Wikipedia and Unicode Tables

Note: Braille Patterns from Wikipedia and Unicode Tables

To convert a two-by-three piece into a braille character, I made a simple util function. This code uses the above logic to resize the image, convert it into braille characters, and color them on the terminal. You can color the terminal output with \033[38;2;{};{};{}m{}\033[38;2;255;255;255m".format(r, g, b chr(output)). For more information, see ANSI Color Escape Code. If you want to try it out, here is the code: anaclumos/tools-image-to-braille

tip

This code uses an ANSI True Color profile with 16M colors. macOS Terminal will not support 16M color; it only supports 256. You can use iTerm2 or VS Code's integrated terminal to see the full color.


3. Converting any video into a series of images

I planned to experiment with different dimensions with the same image, so I wanted to cache the images physically. I decided to use Python OpenCV to do this.

  1. Set basic configurations and variables.
  2. Read the video file.
  3. Create a directory to store the images.
  4. Loop through the video frames.

An example screenshot. I didn't use GPU acceleration, so it took about 19 minutes. I could've optimized this, but this function runs only once for any video, so I didn't bother.

An example screenshot. I didn't use GPU acceleration, so it took about 19 minutes. I could've optimized this, but this function runs only once for any video, so I didn't bother.

4. Convert text streams into formalized subtitle files

I already had the braille conversion tool from section 2; now, I needed to run this function for every cached image. I first tried to use the .srt (SubRip) format. The .srt file looks like this:

1
00:01:00,000 --> 00:02:00,000
This is an example
SubRip caption file.

The first line is the sequence number, and the second is the time range in the Start --> End format ( HH:mm:ss,SSS ). Lastly, the third line is the subtitle itself. I chose SubRip because it supported colored subtitles.

It turned out that SubRip's text stylings are non-standard. Source: en.wikipedia.org

It turned out that SubRip's text stylings are non-standard. Source: en.wikipedia.org

I made several SubRip files with different colors, but YouTube won't recognize the color; it turned out SubRip's color styling is nonstandard.

Types of subtitles YouTube supports

No style info (markup) is recognized in SubRip.

No style info (markup) is recognized in SubRip.

Simple markups are supported in SAMI.

Simple markups are supported in SAMI.

YouTube docs shows the above table. I figured that SAMI files supported simple markups, so I used SAMI. (Oddly enough, I am very familiar with SAMI because .smi is the standard file for Korean subtitles.) Creating subtitles is already simple because it is appending text to a file in a specific format, which didn't require a lot of code change. Microsoft docs shows the structure of SAMI files.

<SAMI>
<HEAD>
<STYLE TYPE = "text/css">
<!--
/* P defines the basic style selector for closed caption paragraph text */
P {font-family:sans-serif; color:white;}
/* Source, Small, and Big define additional ID selectors for closed caption text */
#Source {color: orange; font-family: arial; font-size: 12pt;}
#Small {Name: SmallTxt; font-size: 8pt; color: yellow;}
#Big {Name: BigTxt; font-size: 12pt; color: magenta;}
/* ENUSCC and FRFRCC define language class selectors for closed caption text */
.ENUSCC {Name: 'English Captions'; lang: en-US; SAMIType: CC;}
.FRFRCC {Name: 'French Captions'; lang: fr-FR; SAMIType: CC;}
-->
</STYLE>
</HEAD>
<BODY>
<!<entity type="mdash"/>- The closed caption text displays at 1000 milliseconds. -->
<SYNC Start = 1000>
<!-- English closed captions -->
<P Class = ENUSCC ID = Source>Narrator
<P Class = ENUSCC>Great reason to visit Seattle, brought to you by two out-of-staters.
<!-- French closed captions -->
<P Class = FRFRCC ID = Source>Narrateur
<P Class = FRFRCC>Deux personnes ne venant la r&eacute;gion vous donnent de bonnes raisons de visiter Seattle.
</BODY>
</SAMI>

You can see it's just a simple XML file. Looking closely, you can also see how multi-language subtitles are handled in one SAMI file.


5. Compressing the text files

You would never imagine _compressing_ a _text_ file...

You would never imagine _compressing_ a _text_ file...

I finally got my hands on the SAMI file to discover that the file was over 70MB. I couldn't find any official size limit for YouTube subtitles, but empirically, I discovered the file size limit was around 10MB. So I needed to compress the files.

I thought of three ways to compress the files:

  1. Reduce the width and height.
  2. Skip some frames.
  3. Use color stacks.

I already separated the configurations from the main code, so I could easily change the width, height, and frame rate. However, after many experiments, I figured that YouTube only supports 8—10 frames per second for subtitles, so I decided to skip some frames to reduce the file size.

class braille_config:
# 2 * 3 braille
base = 0x2800
width = 2
height = 3


class video_config:
width = 56
height = 24
frame_jump = 3 # jumps 3 frames

What I mean by "color stacks" is that I could push the same color to the stack and pop it when the color changes. Let's take a look at the original SAMI file:

<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<FONT color="#FFFFFF"></FONT>
<!-- Text Length: 371 -->

Although they are all the same color, the code appended the color tag for every character. Therefore, I can reduce the repetition by using color stacks:

<FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT>
<!-- Text Length: 41. Reduced by 89% -->

It's not the complete-search-maximal-compression you usually see when Leetcoding, but it's still an excellent compression to make it under 10MB. This simple algorithm is especially good when you have black-and-white videos.

<SYNC Start=125><P Class=KOKRCC><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR></SYNC>
<SYNC Start=250><P Class=KOKRCC><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR></SYNC>
<SYNC Start=375><P Class=KOKRCC><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR></SYNC>
<SYNC Start=500><P Class=KOKRCC><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR></SYNC>
<SYNC Start=625><P Class=KOKRCC><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR><FONT color="#FFFFFF">⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿</FONT><BR></SYNC>

The file completed so far: raw.githubusercontent.com/anaclumos/video-in-dots/main/examples/butter.smi (No Dithering)


6. Ditherings

I uploaded the file I created so far, but something was off. It seemed like a problem with how mobile devices handle braille characters. For example, a flat braille character appeared as a circle on computers but as an empty space on mobile devices. (Maybe legibility issues?) I needed extra modifications to resolve this issue: dithering.

Mobile devices show space instead of an empty circle. On the left, you can see almost no details, but on the right, you can see more gradients and details. The right one is the dithered version. Dithering especially shines when you have a black background or color gradients.

Mobile devices show space instead of an empty circle. On the left, you can see almost no details, but on the right, you can see more gradients and details. The right one is the dithered version. Dithering especially shines when you have a black background or color gradients.

The original image from the video. BTS Jimin

The original image from the video. BTS Jimin

Dithering is a technique to compensate for image quality loss when converting an image to a lower color depth by adding noise to the picture. Let me explain it with an example from Wikipedia:

The first image uses 16M colors, and the second and third use 256 colors. Dithered images use compressed color space, but you can feel the details and gradients. Image from en.wikipedia.org

The first image uses 16M colors, and the second and third use 256 colors. Dithered images use compressed color space, but you can feel the details and gradients. Image from en.wikipedia.org

Can you see the difference between the second and third images? They use 256 colors, but the third image has more details and gradients. In this way, we can adequately locate pixels to represent the image properly.

Dithering is also used in GIF image conversion, so most GIF images show many dotted patterns. Digital artifacts are also related to ditherings. You lose some details when you convert an image to a lower color depth. If the dithering happens often, you will get a picture with many artifacts. (Of course, digital artifacts have many other causes. See dithering and color banding for more information.)

Monochrome conversion also requires dithering because we are compressing the 16M color space into two colors. We can do this with the PIL library mentioned above.

resized_image_bw = resized_image.convert("1")  # apply dithering

Let us check this in action.

Can you perceive the difference, especially from 1:33?


Results

I completed the project and uploaded the video to YouTube. I aim to study computer graphics and image processing more further. If you are interested in this topic, please check out my previous post: How Video Compression Works

Butter

Fiesta


Added 2021-07-09: Irregular Subtitle Specs?

I tested the subtitle file on the YouTube app on iOS/iPadOS, and macOS Chrome, Firefox, and Safari. However, I heard that the subtitle file does not work on some devices, like the Android YouTube app and Windows Chrome. I have attached a screen recording of the subtitle file on macOS 11 Chrome 91. You can expect the subtitle file to work when using an Apple device.

I also made the screen recording in 8K to show crisp dots in motion 😉

counting...

TLDR

  • If you have this error, double-check if your rootDir is consistent.
  • I got this error from TSC, auto-flattening the folder structure.

On my TypeScript Node Server, I suddenly got the following error on the tsc command for production settings.

internal/modules/cjs/loader.js:{number}
throw err;

Error: Cannot find module '{project}/dist'
at ... {
code: 'MODULE_NOT_FOUND',
requireStack: []
}

Then I stashed my works and started traveling back in time with git checkout HASH. Comes out, the error started when I added MongoDB Models at src/models.

It seemed strange since it had nothing to do with adding new modules or dependencies. Reinstalling node_modules did not do the job for me (Relevant Stack Overflow Question here). Please take a look at my folder structure.

.
├── LICENSE
├── README.md
├── dist
├── package-lock.json
├── package.json
├── src
│ ├── models (Newly added. Started to cause error.)
│ │ └── user.ts (Newly added. Started to cause error.)
│ └── server
│ ├── config
│ │ ├── config.ts
│ │ ├── dev.env
│ │ ├── dev.env.sample
│ │ ├── prod.env
│ │ └── prod.env.sample
│ └── index.ts
└── tsconfig.json

Long story short, it was the problem in my tsconfig. I have previously declared the following statement on my tsconfig.

{
...
"include": ["src/**/*"]
}

However, since there was only /server folder before creating the model, it seems that TSC has automatically set the root directory to src/server. Therefore the dist output seemed like the following.

dist
├── config
│ ├── config.js
│ └── prod.env
└── index.js

But after models/user.ts was added, src contained both models and server directories, recognizing the root directory as src. So it now became:

dist
├── models
│ └── user.js
└── server
├── config
│ ├── config.js
│ └── prod.env
└── index.js

Notice the directory structure has changed. My entire npm commands were based as if src/server was a root directory (as if the index was at dist/index.js), so that began to cause the error. Therefore I updated the npm commands. Note that I changed dists to dist/servers.

rm -rf dist
&& tsc
- && cp ./src/server/config/prod.env ./dist/config/prod.env
&& export NODE_ENV=prod
- && node dist

rm -rf dist
&& tsc
+ && cp ./src/server/config/prod.env ./dist/server/config/prod.env
&& export NODE_ENV=prod
+ && node dist/server

To prevent TSC from guessing the root directory, you can add the following line on your tsconfig.json.

{
"compilerOptions": {
...
"rootDir": "src",
}
}

This line will retain the absolute folder structure from src.