The Making of: Lyricova Screensaver v2

Preview

It has been long since last time I post here. This time I’d like to share about how I made the Lyricova Screensaver v2: refractor from my own legacy code.

Project Lyricova is my first blog-like system, written in 2013, based on CodeIgnitor, a PHP framework. Initially codenamed Project GY, from its Chinese name 歌语. It has ran as my own lyrics collection site “stably” for years. I made my first screensaver, just simply load up lyrics on the screen, with some text animations, on top of a three.js official demo. With the 3D effect, and multiple concurrent timelines, it can sometimes be laggy, and resource comsuming, and the design is also not as artistic as well. So, this year I came up with my second design.

0x00: Design

Designs

This was a part of my Lyricova redesign. Turns out this is the first that becomes something works. Without a reason, I’m kinda into those typing effects, especially the 上屏 (zh-CN), and 変換 (ja-JP) part of it. (That’s something not really available when you type in alphabetical languages, so I decide not to translate it.)

After the design, still something is missing from the system, namely:

  • Categories
  • Languages isolation
  • Romanization (for typing animations)

0x01: Refractor

So let’s fix them one by one.

0x010: Categories

This is also something I’ve been planning years ago. With mainly Vocaloid lyrics, I chose to use the name of Hatsune Miku Appends + Core for categories. This is actually inspired by the PV of The Division and Destruction of Hatsune Miku (初音ミクの分裂→破壊).

To assign categories to 300+ posts at once is not a easy job. What I did is write up a new page, I call it “Category roller”.

Category roller

With the screenshot, it should be easy to understand. There’s a “keyboard” at the bottom, and a card above. and for pagination, other buttons are just category toggles. Behind that is just an HTTP API, and a script keep updating the location hash (window.location.hash), so it’s easy for me to get back to where I stopped.

Back to the database, I created a postmeta table: a simple Post ID-key-value storage for all future post-related possible values, and of course — categories — in a JSON string.

0x011: Language isolation

Not sure if I used the correct word. :/

In the legacy Lyricova database structure, lyrics are stored in 3 columns of the table: lyric, origin and translate, which supposed to be corresponding to:

  • The language displayed as the main one,
  • The original language of the song (empty if used as the main one), and
  • Other translated versions.

Despite all data is constrained to en, ja and zh, many entries contains mixed languages. To apply romanization to different languages, I have to isolate them from the database.

The method I used is not really fool-proof, but should work in most cases. It’s just simply check for different type of characters. The priority goes as “Kana → Han → Latin”, that is:

  • ja if there’s Kana,
  • zh if there’s Han characters,
  • en otherwise.

Source code: lyricproc.py

0x012: Romanization

This is really the most difficult part of this whole thing, as it even involves NLP. The ideal case is that it not only correctly revert the text to the “typing string”, but also sector it as how we usually type.

For Japanese text, I used MeCab for the job. Aside from getting the “yomi” of each word, it also gives a value that reflects the connection between 2 words. By trial and error, a difference of -1000 produces a pretty acceptable result for phrase grouping.

However, for Chinese text, JieBa and pypinyin can generate the Pinyin string quite well, but JieBa — based on dictionary and graph scanning — doesn’t really provide a relation between words for grouping. So only the minimum word grouping is used.

Source code: tr.py

BTW, this script is written to support shell arguments for future uses.

Updating the admin panel is trivial, so I won’t talk too much about it.

0x02: Screensaver

Like the last one, it’s still done in HTML, simply because it the most easy way to build a cross platform GUI application, especially with the spread of modern browsers. Luckily the built-in WebKit in OS X 10.11 still can handle those HTML5 features.

0x020: Lyrics Data

Although there’s API on the server where I can get all the necessary lyrics data, I can’t let it ask from the server everytime it loads. You may want to say there is Local Storage, but (there’s always a but)…

OS X’s screensaver WebView seems to drop LocalStorage content every time it launches. (Possibly due to the sandbox mechanism.)
It works fine when you preview it, but not when you actually trigger it.

As I’m not planning to install it in OS-level elsewhere, and PHP (on Apache) is always running on my laptop, I used Flintstone, a simple file based key-value storage, wrapped up in a tiny PHP script accepting GETs and POSTs.

Source code: storage.php

0x021: Animation

It’s pretty simple for Latin scripts, and Chinese text with Pinyin string, but more works required for Japanese even with its Yomi. Inspired by JaTicker, a Japanese typing animation jQuery plugin, I write up a logic for generating the animation sequence for any random Hiragana Yomi, in a rather natural way.

0x022: Data update

Everytime when the screensaver starts, it queries the last update timestamp from the remote server and compares with the local timestamp, if there’s anything updated, all updated entries will be loaded from server, and overwriting the local entries.

0x023: Design (CSS)

JS scripts has already taken care of the content change, the CSS part should be simple. The blinking cursor is done with CSS animation, everything else is in flexboxes. That’s all.

0x024: Update config.

Considering the sandbox issue on OS X system screensavers, I have to resort to location hash for updating the configuration. The screensaver takes a JSON object which overrides the config dict, which includes:

1
2
3
4
5
6
{
"local_storage_api": false, // False for alternate LocalStorage HTTP API
"zh": [], // Category filter per language, empty for none,
"en": ["all"], // "all" for all categories
"ja": ["core", "bright"] // or only allow certain categories
}

0x03: System screensaver

The screensaver wrapper I used is WebViewScreensaver on OS X. It works pretty well here, other than the local storage problem. Remember to #{"local_storage_api":false} to your URL.