玩式草子─ソフトウェアとたわむれる日々

第64回お家通信カラオケシステムを作ろう[その3]

前回、通信カラオケ用の検索システムを作るために、VHDディスクから拾った楽曲リストの曲名と歌手名にひらがなの読みを追加する作業について紹介しました。今回は、そうやって作った楽曲リストをデータベースに登録して、実際に動作する通信カラオケシステムを作ってみます。

楽曲リストの校正

前回紹介したように、まずはkakasiを使って楽曲リストの曲名と歌手名にひらがなの読みを付けてみました。

kakasiは、SKKプロジェクトが蓄積した辞書ファイル(ひらがなと漢字の対応リスト)を元に、漢字からひらがなの読みを引けるデータベースをあらかじめ用意しておき、与えられた漢字かな混り文をこのデータベースと照合して、マッチする単語があればその読みを返す、という単純な仕組みになっています。

この変換方法はシンプルではあるものの、データベースに登録されている単語の有無によって読みが左右されるという問題があります。たとえば「中島みゆき」という歌手名をkakasiでひらがなに変換すると、⁠なかしまみゆき」になってしまいます。

$ echo '中島みゆき' | kakasi -JH
なかしまみゆき

この誤変換はkakasiが使っているデータベースに「中島」「なかじま」と読ませる読みが登録されていないことが原因です。⁠中島」が無いため、⁠中島」「中」「島」に分解され、⁠なか」⁠しま」という読みが返されるため、⁠なかしまみゆき」になってしまいます。

この問題は、kakasiが利用するデータベースに「中島:なかじま」という読みを登録してやれば解決するものの、データベースに新しい読みを追加するためには、元となった辞書ファイルに必要な読みを追加した上でmkkanwadictコマンドを使ってkakasi用のデータベースを再生成する必要があって、ちょっと面倒です。

そのため人名やグループ名のような固有名詞にはkakasiを使わず、自前で読みを振るような機能を追加してみました。

  1 # -*- coding: euc-jp -*-;
  2 
  3 def set_namedict():
  4     namedict = {
  5         'ANN':'あん',
  6         'ASKA':'あすか',
  7         "B'Z":'びーず',
  8         'BE-B':'びーび',
... 
110         '梶芽衣子':'かじめいこ',
111         '米米CLUB':'こめこめくらぶ',
112         '千昌夫':'せんまさお',
113         }
114 

"ANN"や"ASKA"といった英語表記の歌手名にはそれぞれひらがなの読みを対応させると共に、誤変換が目についた「梶芽衣子」「千昌夫」といった人名にもそれぞれ適切な読みを対応させています。

やっていることは簡単で、まずは上記のようなset_namedict()関数を用意して、namedict{}に表記と読みの対応リストを登録していきます。

そして、前回紹介したスクリプトで歌手名にふりがなを付ける際、namedict{}にある名前にはkakasiからではなく、対応リストから得られる読みを使うようにしました。

50        singer2 = singer.upper()
51        
52        if namedict.has_key(singer2):
53            singer_yomi = namedict[singer2]
54        else:
55            singer_yomi = to_hiragana(singer2)

なお、曲目リストに記録した歌手名のアルファベット表記には大文字と小文字の表記揺れがあったので、揺れを吸収するために歌手名はいったん大文字に統一して(50行め⁠⁠、その上でname_dict{}を引くようにしています。

目につくアルファベット表記や誤変換をnamedict{}に登録してはスクリプトを再実行する、という作業を繰り返して、機械的にひらがなの読みを付けた楽曲リストは完成しました。

  1-1-01 | 愛しつづけるボレロ | あいしつづけるぼれろ | 五木ひろし  | いつきひろし        | 阿久悠              | 筒美京平             |
  1-1-02 | 会津の小鉄         | あいづのこてつ       | 京山幸枝若 | きょうやまこうしわか | 松島一夫            | 和田香苗             |
  1-1-03 | 勘太郎月夜唄       | かんたろうつきようた | 小畑実     | おばたみのる         | 佐伯孝夫            | 清水保雄             |
  1-1-04 | 青葉城恋唄         | あおばじょうこいうた | さとう宗幸 | さとうむねゆき       | 星間船一            | さとうむねゆき
  ...

歌手名は複数回出てくる可能性があるのでスクリプトレベルで対応した方がいいものの、タイトルの当て字読みやアルファベット表記はたいてい1回限りしか出てこないので、namedict{}に登録して直すよりも出力結果を直接修正する方が楽でしょう。そう考えて、楽曲リストをさらに手動で直していくことにしました。

  ...
  8175-2-01 | 名もなき詩(うた) | めいもなきし(うた) | Mr.Children  | みすたーちるどれん | 桜井和寿 | 桜井和寿 
  8175-2-02 | FOREVER           | FOREVER             | 岡本真夜     | おかもとまよ       | 岡本真夜 | 岡本真夜 
  8175-2-02 | river             | river               | CHAGE&ASKA  | ちゃげあんどあすか | 飛鳥涼   | 飛鳥涼 
  8175-2-04 | 幸せになりたい     | しあわせになりたい   | 内田有紀     | うちだゆき         | 広瀬香美 | 広瀬香美 
  ...

「ああ、この頃はバンドブームだったなぁ…」⁠この頃は小室哲也の絶頂期だっけ…」などと追憶にふけりつつ、⁠めいもなきし(うた⁠⁠」を「なもなきうた」に直したり、"Forever"に「ふぉーればー」と読みを付けたりする退屈な作業を進めました。

データベースの構築

ひらがな読みを付けた楽曲リストの修正を一通り終えてから、このリストをSQLite3のデータベースに登録するためのPythonスクリプトを書きました。

 1 # -*- coding: euc-jp -*-;
 2 
 3 import sqlite3, os, sys
 4 
 5 def init_db(dbname):
 6     conn = sqlite3.connect(dbname)
 7     conn.isolation_level = None
 8     cursor = conn.cursor()
 9     cursor.execute('''create table titles
10        (id text, title text, title_yomi text, singer text, singer_yomi text, songwriter, composer)''')
11     return conn
12 
13 def insert_db(cursor, t):
14     try:
15         # print "inserting ", t
16         cursor.execute('insert into titles values(?, ?, ?, ?, ?, ?, ?)', t)
17     except sqlite3.Error, e:
18         print "An error occurred:", e.args[0]
19 
20 def main():
21     dbname = 'karaoke-titles.sql'
22 
23     if os.path.isfile(dbname) == False:
24         connection = init_db(dbname)
25     else:
26         connection = sqlite3.connect(dbname)
27     cursor = connection.cursor()
28 
29     datafile = 'karaoke-yomi.dat'
30     if os.path.isfile(datafile):
31         f = open(datafile, 'r')
32         lines = f.readlines()
33         for line in lines:
34             dt = line.split(" | ")
35             if len(dt) != 7:
36                 print("Data count {} error at {}:".format(len(dt), line))
37                 continue
38                 
39             id          = dt[0]
40             title       = dt[1].decode('euc-jp')
41             title_yomi  = dt[2].decode('euc-jp')
42             singer      = dt[3].decode('euc-jp')
43             singer_yomi = dt[4].decode('euc-jp')
44             songwriter  = dt[5].decode('euc-jp')
45             composer    = dt[6].decode('euc-jp')
46 
47             insert_db(cursor, (id, title, title_yomi, singer, singer_yomi, songwriter, composer))
48 
49 if __name__ == "__main__":
50     main()

このスクリプトでは、1行に7つのテキスト形式のコラムを持つSQLite3形式のデータベース("karaoke-titles.sql")を作り、そこにひらがなの読みをつけた楽曲リスト("karaoke-yomi.dat")のデータを流し込んでいます。これくらいの処理ならスクリプトを書くまでもないかな、とも思いましたが、データに修正が必要な場合は楽曲リストに戻って修正し、データベースも再生成することになるので、スクリプト化しておく方が作業をくりかえすには楽でしょう。

スクリプトを使って作成したデータベースを確認するために、シェルからsqlite3コマンドを使って操作してみます。なお、SQLite3の扱うテキストデータはUTF-8形式なので、端末ソフトの文字コードもUTF-8に変更しています。

 $ sqlite3 karaoke-titles.sql 
 SQLite version 3.8.5 2014-06-04 14:06:34
 Enter ".help" for usage hints.
 sqlite> select id,title,singer from titles where singer_yomi like "なか%";
    14-2-11|かもめはかもめ      |中島みゆき          
    20-2-02|秋冬                |中山丈二            
    23-2-06|悪女                |中島みゆき          
    26-1-09|ひとり              |中島みゆき          
   305-2-10|十戒(1984)          |中森明菜            
 ...
  8222-1-04|鳶                  |中村美律子          
     9-2-03|船頭小唄            |中山歌子

ざっと見、データベースは正しく作成できているようなので、Web経由でこのデータベースを利用するためのコードを書く段階に移りました。

検索システムの構築

今回考えているお家通信カラオケシステムは、このデータベースを使ってタイトルや歌手名からその曲のID(=ファイル名)を調べ、HTML5の<video>タグを使ってその動画ファイルを再生させる、という単純な仕様です。前々回、<video>タグを使ってブラウザ経由で動画ファイルを再生する部分は考えたので、残りは検索キーワードを入力する画面と検索結果を表示する画面です。

前回紹介したセガカラの検索画面にならって、検索キーワードを入力する画面は、フリーワード入力、あるいは五十音表から歌手名やタイトルの1文字目を入力する、という構成にしてみました。

図1 お家カラオケ検索ページ
図1 お家カラオケ検索ページ

見ての通りのシンプルな画面構成で、PHPのコードにして140行弱、多少考えたのは五十音表を組むあたりで、PHPの2次元配列にいれた五十音を、<table>の中に入れ子状にして、清音と濁音を横に並べる、という形にしてみました。参考までにコードの前半部分を紹介しておきます。

 1  <!DOCTYPE html>
 2  <HTML>
 3  <head>
 4  <meta charset="UTF-8">
 5  <title>kraoke keyword search</title>
 6  </head>
 7  <BODY>
 8  
 9  <H2>フリーワード検索</H2>
10  
11  検索キーワードを入れてください.
12  
13  <form action="search.php">
14  <p>
15  <input type="radio" name="kind" value="title_yomi" checked> タイトル
16  <input type="radio" name="kind" value="singer_yomi"> 歌手名
17  </p>
18  <p>
19  キーワード
20  <input type="text" name="key">
21  </p>
22  </form>
23  
24  <H2>タイトル検索</H2>
25  
26  曲のタイトルの一文字目を指定してください.
27  
28  <table border>
29  <tr> <td>
30  <table border>
31  <?php
32  $kana = array( ['あ', 'い', 'う', 'え', 'お'],
33                 ['か', 'き', 'く', 'け', 'こ'],
34                 ['さ', 'し', 'す', 'せ', 'そ'],
35                 ['た', 'ち', 'つ', 'て', 'と'],
36                 ['な', 'に', 'ぬ', 'ね', 'の'],
37                 ['は', 'ひ', 'ふ', 'へ', 'ほ'],
38                 ['ま', 'み', 'む', 'め', 'も'],
39                 ['や', 'ゆ', 'よ'],
40                 ['ら', 'り', 'る', 'れ', 'ろ'],
41                 ['わ', 'ん']
42          );
43  for($i = 0; $i < 10; $i++) {
44      echo "<tr> ";
45      foreach ($kana[$i] as $k) {
46          printf("<td> <a href=\"search.php?key=%s&kind=title_yomi\"> %s <a> </td>\n", $k, $k);
47      }
48      echo "</tr>\n";
49  }
50  ?>
51  </table>
52  </td>
53  <td>
54  <table border>
55  <?php
56  $kana2 = array(['が', 'ぎ', 'ぐ', 'げ', 'ご'],
57                 ['ざ', 'じ', 'ず', 'ぜ', 'ぞ'],
58                 ['だ', 'ぢ', 'づ', 'で', 'ど'],
59                 ['ば', 'び', 'ぶ', 'べ', 'ぼ'],
60                 ['ぱ', 'ぴ', 'ぷ', 'ぺ', 'ぽ']  
61          );
62  
63  for($i = 0; $i < 5; $i++) {
64      echo "<tr> ";
65      foreach ($kana2[$i] as $k) {
66          printf("<td> <a href=\"search.php?key=%s&kind=title_yomi\"> %s <a> </td>\n", $k, $k);
67      }
68      echo "</tr>\n";
69  }
70  for($i = 5; $i < 10; $i++) {
71      echo "<tr> <td colspan=5> <br> </td> </tr>\n ";
72  }
73  ?>
74  </table>
75  </td>
76  </tr>
77  </table>
...

このページではタイトルの読みから引くか歌手名の読みから引くかをkindという引数に、検索すべきキーワードをkeyという引数に収めて、search.phpを呼び出します。search.php の処理は50行弱なので全文を紹介しておきましょう。

 1  <!DOCTYPE html>
 2  <html>
 3  <head>
 4  <meta charset="UTF-8">
 5  <title>karaoke keyword search</title>
 6  </head>
 7  <body>
 8  
 9  <?php
10  function DBconnect($name) {
11     $dbname = sprintf("sqlite:%s", $name);
12     try {
13         $db = new PDO($dbname);
14     }
15     catch (PDOException $e) {
16         exit("cannot connect to DB".$e->getMessage());
17     }
18     return $db;
19  }
20  
21  function query_db($db, $kind, $key) {
22      $sql = sprintf('select id,title,singer from titles where %s like "%s%%" order by %s', $kind, $key, $kind);
23      foreach ($db->query($sql) as $line) {
24          $item = array(
25                        "id" => trim($line['id']),
26                        "title" => $line['title'],
27                        "singer" => $line['singer']
28                       );
29          $results[] = $item;
30     }
31     return $results;
32  }
33      
34  $dbname = './karaoke-titles.sql';
35  $kind = $_GET['kind'];
36  $key = $_GET['key'];
37  
38  echo "kind:$kind <br>";
39  echo "key:$key <br>";
40  
41  $db = DBconnect("./karaoke-titles.sql");
42  $res = query_db($db, $kind, $key);
43  
44  foreach ($res as $line) {
45        printf("<a href=\"play.php?id=%s\">%s (%s)</a><br>\n", $line['id'], $line['title'], $line['singer']);
46  }
47  
48  ?>

10行目からのDBconnect()関数でSQLite3データベースに接続し、21行目からのquery_db()関数で

  select id,title,singer from titles where singer_yomi like "なか%" order by singer_yomi

のようなSQL文を作ってデータベースを検索し、その結果を$result[]という配列に入れて返します。$result[]には見つかった曲のID(=ファイル名)とタイトル等の情報が入っているので、44行目からのループでそれらを表示します。

図2 検索結果の表示ページ
図2 検索結果の表示ページ

それぞれの検索結果には、動画ファイルを再生するためのリンクを張っており、再生したいタイトルをクリックすればブラウザから目的の楽曲が再生されます。

図3 楽曲の表示ページ
図3 楽曲の表示ページ

ちなみに動画ファイルを再生するplay.phpは20行ほどのコードです。

 1  <!DOCTYPE html>
 2  <html>
 3  <head>
 4  <meta charset="UTF-8">
 5  <title>karaoke playback</title>
 6  </head>
 7  <body>
 8  
 9  <?php
10  $id = $_GET['id'];
11  $file = "./MP4/".$id.".mp4";
12  echo "file: $file";
13  ?>
14  
15  <video autoplay controls>
16  <?php
17      printf("<source src=\"%s\">", $file);
18  ?>
19  </video>
20  
21  </body>
22  </html>

当初は、結構な規模が必要かな、と思っていたものの、最近のPHPやHTML5を使うと、⁠お家通信カラオケシステム」に必要な最低限の機能は、200行ちょっとのコードで実現できるようです。とりあえず動くシステムはできたので、今後は機能の追加や動画ファイルの変換作業を進めていくことになるでしょう。


今回、コードやデータを玩(もてあそ)んでいてあらためて感じたのは、漢字やひらがな、カタカナに加えて、アルファベットまで貪欲に飲み込む、日本語の表記システムの柔軟さです。

"Forever"を「フォーエバー」と表記するか、英語のまま表記するか、あるいは「永遠」「永久」という漢語を使うか、そこに「とわ」「とこしえ」という和訓を付けるか、それぞれの表記によって喚起されるイメージは異なるでしょう。⁠世界最短の文学」と呼ばれる俳句や短歌は、このような日本語の柔軟性を最大限に活用した芸術です。

いっぽう、このような表記の柔軟性はコンピュータで処理する際には足枷ともなります。今回は「キーワードにひらがなで読みを振る」という力技で切り抜けたものの、データの規模が増えると、そのような処理では追い付かなくなるでしょう。日本語の持つ柔軟性やイメージ喚起力を保ちつつ、コンピュータで効率よく扱うにはどうすればいいか、そこには文字コードや正書法を越えた課題があるように思います。

おすすめ記事

記事・ニュース一覧