Closed8

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 フォルダを選択する。

このスクラップは2021/03/31にクローズされました
ログインするとコメントできます