kagomeをWASMでうごかす
手元で確認するためにサーバを立てるのはめんどいので python を利用する
python -m SimpleHTTPServer 8080
これだと、MIME で問題が起こるので、サーバが application/wasm
を理解するようにしないといけない。
ネットに落ちてたコードに適当に wasm を足す。
# -*- coding: utf-8 -*-
import http.server
from http.server import HTTPServer, BaseHTTPRequestHandler
import socketserver
PORT = 8080
Handler = http.server.SimpleHTTPRequestHandler
Handler.extensions_map={
'.manifest': 'text/cache-manifest',
'.html': 'text/html',
'.png': 'image/png',
'.jpg': 'image/jpg',
'.svg': 'image/svg+xml',
'.css': 'text/css',
'.js': 'application/x-javascript',
'.wasm': 'application/wasm',
'': 'application/octet-stream', # Default
}
httpd = socketserver.TCPServer(("", PORT), Handler)
print("serving at port", PORT)
httpd.serve_forever()
これを起動する。
python3 server.py
Go 1.6 を利用。wasm で必要なファイルを用意しておく。
$ ls ~/sdk/go1.16.2/misc/wasm/
go_js_wasm_exec* wasm_exec.html wasm_exec.js
wasm_exec.js
を html で指定する必要がある。html の書き方は wasm_exec.html を参考にやればよい。
Go で wasm 用のプログラムを書く。Goland を利用していると syscall/js
をインポートしてるとターゲットが合わないとか言われてしまうが、適当に置き換える。
辞書を読み込むタイミングが実行時だと、最初だけ重くなってしまうので、Callback を設定するときに辞書を言ったん読み込んでおくことにした。形態素解析結果は []map[string]string
の形式で返すが、返値としている ret
をこの形式で宣言すると js.ValueOf()
でエラーになる。[]interface{}
で宣言してから、map[string]interface{}
を詰め込むようにする。mapの value が interface{}
になっているからといって []string
を埋め込んだりしてもエラーになるようだ。
ブラウザで実行したときに ValueOf: invalid value
とメッセージが出ている場合は変換で何か起こっている可能性が高そう。結構ハマった。変換の場合分けにない型を直接利用しなければよさそうではある。
func ValueOf(x interface{}) Value {
switch x := x.(type) {
case Value: // should precede Wrapper to avoid a loop
return x
case Wrapper:
return x.JSValue()
case nil:
return valueNull
case bool:
if x {
return valueTrue
} else {
return valueFalse
}
case int:
return floatValue(float64(x))
case int8:
return floatValue(float64(x))
case int16:
return floatValue(float64(x))
case int32:
return floatValue(float64(x))
case int64:
return floatValue(float64(x))
case uint:
return floatValue(float64(x))
case uint8:
return floatValue(float64(x))
case uint16:
return floatValue(float64(x))
case uint32:
return floatValue(float64(x))
case uint64:
return floatValue(float64(x))
case uintptr:
return floatValue(float64(x))
case unsafe.Pointer:
return floatValue(float64(uintptr(x)))
case float32:
return floatValue(float64(x))
case float64:
return floatValue(x)
case string:
return makeValue(stringVal(x))
case []interface{}:
a := arrayConstructor.New(len(x))
for i, s := range x {
a.SetIndex(i, s)
}
return a
case map[string]interface{}:
o := objectConstructor.New()
for k, v := range x {
o.Set(k, v)
}
return o
default:
panic("ValueOf: invalid value")
}
}
Go のプログラム
ビルドは以下のようにする:
GOOS=js GOARCH=wasm go build -o kagome.wasm main.go
package main
import (
"strings"
"syscall/js"
"github.com/ikawaha/kagome-dict/ipa"
"github.com/ikawaha/kagome/v2/tokenizer"
)
func igOK(s string, _ bool) string {
return s
}
func tokenize(_ js.Value, args []js.Value) interface{} {
if len(args) == 0 {
return nil
}
t, err := tokenizer.New(ipa.Dict(), tokenizer.OmitBosEos())
if err != nil {
return nil
}
var ret []interface{}
tokens := t.Tokenize(args[0].String())
for _, v := range tokens {
//fmt.Printf("%s\t%+v%v\n", v.Surface, v.POS(), strings.Join(v.Features(), ","))
ret = append(ret, map[string]interface{}{
"word_id": v.ID,
"word_type": v.Class.String(),
"word_position": v.Start,
"surface_form": v.Surface,
"pos": strings.Join(v.POS(), ","),
"base_form": igOK(v.BaseForm()),
"reading": igOK(v.Reading()),
"pronunciation": igOK(v.Pronunciation()),
})
}
return ret
}
func registerCallbacks() {
_ = ipa.Dict()
js.Global().Set("kagome_tokenize", js.FuncOf(tokenize))
}
func main() {
c := make(chan struct{}, 0)
registerCallbacks()
println("Kagome Web Assembly Ready")
<-c
}
形態素解析をするペライチの HTML
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<style type="text/css">
body {
text-align: center;
}
div#center{
width: 800px;
margin: 0 auto;
text-align: left;
}
.tbl{
width: 100%;
border-collapse: separate;
}
.tbl th{
width: 20%;
padding: 6px;
text-align: left;
vertical-align: top;
color: #333;
background-color: #eee;
border: 1px solid #b9b9b9;
}
.tbl td{
padding: 6px;
background-color: #fff;
border: 1px solid #b9b9b9;
}
.frm {
min-height: 10px;
padding: 0 10px 0;
margin-bottom: 20px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
}
.txar {
border:10px;
padding:10px;
font-size:1.1em;
font-family:Arial, sans-serif;
border:solid 1px #ccc;
margin:0;
width:95%;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: inset 0 0 4px rgba(0,0,0,0.2);
-webkit-box-shadow: inset 0 0 4px rgba(0,0,0,0.2);
box-shadow: inset 0 0 4px rgba(0,0,0,0.2);
}
#box {
width:100%;
margin:10px;
auto;
}
</style>
<meta charset="utf-8">
<title>Kagome WebAssembly Demo - Japanese morphological analyzer</title>
</head>
<body>
<!-- loading... -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pace/1.0.2/pace.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pace/1.0.2/themes/black/pace-theme-corner-indicator.min.css" />
<!-- wasm kagome -->
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
WebAssembly.instantiateStreaming(fetch("kagome.wasm"), go.importObject).then((result) => {
go.run(result.instance);
}).catch((err) => {
console.error(err);
});
</script>
<div id="center">
<h1>Kagome WebAssembly Demo</h1>
<!--
<a href="https://github.com/ikawaha/kagome.ipadic/blob/gh-pages/wasm_sample.go">=>source code</a>
-->
<form class="frm" oninput="tokenize()">
<div id="box">
<textarea id="inp" class="txar" rows="3" name="s" placeholder="Enter Japanese text below."></textarea>
</div>
</form>
<table class="tbl">
<thread><tr>
<th>Surface</th>
<th>Part-of-Speech</th>
<th>Base Form</th>
<th>Reading</th>
<th>Pronunciation</th>
</tr></thread>
<tbody id="morphs">
</tbody>
</table>
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.0/jquery.min.js"></script>
<script>
function tokenize() {
var s = document.getElementById("inp").value;
ret = kagome_tokenize(s)
$("#morphs").empty();
$.each(ret, function(i, val) {
console.log(val);
var pos = "*", base = "*", reading = "*", pronoun = "*";
$("#morphs").append(
"<tr>"+"<td>" + val.surface_form + "</td>" +
"<td>" + val.pos + "</td>"+
"<td>" + val.base_form + "</td>"+
"<td>" + val.reading + "</td>"+
"<td>" + val.pronunciation + "</td>"+
"</tr>"
);
});
}
</script>
</body>
</html>
ディレクトリ構成
.
├── kagome.html
├── kagome.wasm
├── main.go
├── server.py
└── wasm_exec.js
gh-pages で公開する
docs というフォルダを作成して、そこに一通りのファイルを配置。Settings -> GitHub Pages でブランチと docs
フォルダを選択する。