0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

非エンジニアが商談用のリアルタイム通訳ツールを作ったら、macOSの壁が5枚あった

0
Posted at

海外との商談が増えて、困っていたこと

僕はエンジニアではありません。株式会社コホマダという会社で、海外企業の日本市場参入と、日本企業の海外展開を支援しています。前回、Claude Code で自社サイトを作った話を書きました。今回はその続きではなく、まったく別のものを作った話です。

仕事柄、海外との打ち合わせが多くなりました。英語が中心ですが、ポルトガル語の商談もあります。先日は101分、通訳を挟んで話しました。

先に断っておくと、これから作るものは「翻訳がないと商談できない」から作ったのではありません。通訳が必要な場面には通訳に入ってもらいますし、英語の打ち合わせは普通にやっています。そこは仕事なので、機械に任せる話ではない。

困っていたのは、別のところでした。

通訳が訳さない部分がある

直近でポルトガル語の案件がありました。通訳に入ってもらったので、本題は問題なく理解できます。そこは通訳の仕事で、正確にやってもらえます。

ただ、通訳が訳すのは本題です。

話の合間にこぼれる一言や、本題から少しそれた雑談まではカバーされません。それは通訳の職分の外なので、当然のことです。

問題は、そこに大事なものが混ざることでした。

「うちも似たことで困っていて」「前に一度やって失敗した」。そういう一言は、たいてい議題から外れたところで出ます。日本語の商談なら確実に拾って、その場で掘り下げているはずの場面です。

それが、訳されないまま流れていく。あとで議事録を見ても、そこは要約されて消えているか、そもそも記録に残っていません。

拾いたかったのは、ここでした。

議事録は残る。でも、その場では読めない

打ち合わせの録画と文字起こしは、ツールを入れて残しています。終わったあとに見返せば、何を話したかは分かる。

ただ、それは終わってから読むものです。

商談で本当に必要なのは、相手が話しているその瞬間に「いま何を求められたのか」を掴むことです。議事録は、そこには間に合いません。

そして外国語だと、この「その場で掴む」が一段むずかしくなる。聞き取ることに意識を持っていかれて、意味を組み立てる余裕が削られる。理解が半歩遅れて、こちらが考えている間に次の話に進んでいる。

トーンは、文字にならない

もうひとつ。同じ言葉でも、言い方で意味が変わります。

「それは難しいですね」を、切り捨てるように言ったのか、迷いながら言ったのか。後者なら、まだ余地があります。前者なら、そこは引くべきところです。

耳では、その差は分かります。日本語ならほぼ無意識に処理している情報です。ところが外国語だと、言葉を追うので手一杯になって、トーンまで意識が回らないことがある。

だから、言葉のほうを機械に引き受けてもらいたかった。文字は画面に出ているから、耳は相手の言い方に集中できる。そういう状態を作りたかったんです。

認識のズレに、あとから気づく

そしてこれが一番厄介でした。

その場では通じたつもりでいて、後日のメールで噛み合っていないことが分かる。「そちらが確認すると言いましたよね」「いえ、こちらが確認すると申し上げました」。

これは語学力の問題ではありません。省略された主語や、曖昧なまま流した条件が、あとで別の意味で立ち上がってくる。日本語同士でも起きることが、外国語だと確率が上がるだけです。

議事録を見返せば、どちらが正しいかは分かります。ただ、そのときにはもう一往復増えている。その場で「いま、こう受け取りましたが合っていますか」と確認できていれば、起きなかった話です。

欲しかったのは、補助輪でした

つまり僕が欲しかったのは、通訳の代わりでも、議事録の代わりでもありません。

その場で、いま言われたことを目で確認できる状態です。耳は相手の言い方に向けたまま、言葉のほうは画面で拾う。理解がずれていれば、その場で聞き返せる。

補助輪です。走るのは自分で、外れかけたときに支えてくれるもの。

構成としては、こうなります。

イヤホンで相手の声を普通に聞きながら、同じ音を裏で拾って、日本語にして画面に出す。

作ってみたら、動きました。ただ、そこまでに壁が5枚ありました。全部 macOS の壁で、コードの問題ではありませんでした。この記事は、その5枚の話です。

最初に、何を諦めたか

「ブラウザでできないの?」と最初に思いました。

できません。

macOS のブラウザは、自分のタブの音しか取れません。Zoom や Teams で鳴っている音には手が届かない。これは OS の制約なので、実装をどう変えても回避できません。

なので構成はこうなりました。

Zoom / Meet / Teams の音
      │  ← ここだけネイティブ(Swift)
      ▼
音声の取り込み+文字起こし
      │  ローカルのTCP
      ▼
Node のサーバー(依存パッケージ 0)
      │
      ▼
ブラウザの画面  ← 自分が見るのはここだけ

画面はブラウザです。音の取得だけ、どうしてもネイティブにせざるを得なかった。

壁① 「音を録る」が「画面を録る」に化ける

macOS でシステム音声を取る方法はいくつかあります。BlackHole のような仮想オーディオデバイスを入れる方法が有名で、実際このMacにはもう入っていました。

ただこれ、出力先を「複数出力装置」に切り替える必要があります。 イヤホンの音量調整が効かなくなったりする。会議の直前にオーディオ設定をいじるのは、正直こわい。

そこで ScreenCaptureKit を使いました。出力先を一切変えずに、鳴っている音だけ取れます。 聞こえ方が変わらないのが決定的でした。

ただし、ここで妙なことが起きます。

macOS はシステム音声の取得を「画面収録」の権限に含めています。

映像は要らないので、取得サイズは 2×2 ピクセルにしました。

let cfg = SCStreamConfiguration()
cfg.capturesAudio = true
cfg.width = 2; cfg.height = 2      // 映像は使わないので最小

実質なにも映していません。それでも OS から見れば「画面収録するアプリ」なので、メニューバーにインジケータが点きます。

これは仕様なので、受け入れるしかありませんでした。

壁② Info.plist は、置くだけでは読まれない

文字起こしには macOS 内蔵の Speech フレームワークを使いました。英語と日本語は端末内で完結するので、音声が外に出ません。商談の中身を扱う以上、ここは譲れませんでした。

利用には許可が要ります。Info.plist に説明文を書く、というのは調べればすぐ出てきます。

書きました。落ちました。

This app has crashed because it attempted to access privacy-sensitive
data without a usage description. The app's Info.plist must contain an
NSSpeechRecognitionUsageDescription key...

書いてあるのに「書け」と言われる。しばらく意味が分かりませんでした。

答えは、実行ファイルの中に焼き込む必要があるというものでした。.app/Contents/Info.plist に置いただけでは、この起動のされ方だと読まれません。

swiftc -O -parse-as-library MeetingCore.swift \
  -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist \
  -Xlinker Info.plist \
  -o "$APP/Contents/MacOS/MeetingCore"

この -sectcreate の一行です。これを消すと、また同じところで落ちます。

壁③ ターミナルから起動すると、必ず落ちる

焼き込んでも落ちました。

いろいろ試した結果、こういうことでした。

./MeetingCore.app/Contents/MacOS/MeetingCore   # 落ちる
open MeetingCore.app                            # 動く

アプリとして起動されたときだけ、許可が通ります。 ターミナルから直接叩くと、同じバイナリでも拒否されます。

これは地味に困りました。open で起動すると、標準出力が拾えないからです。文字起こしの結果をどうやって受け取るのか。

結局、ローカルの TCP に流すことにしました。

// 音声認識の許可は、LaunchServices 経由で .app として
// 起動したときだけ通る。つまり stdout は使えない。
// なので結果はローカルの TCP へ流す。
Sink.shared.connect(port: port)

設計がねじれているように見えますが、そうせざるを得なかったという話です。

壁④ 許可ダイアログが、出てこない

裏方アプリにしたかったので LSUIElementtrue にしていました。Dock に出ないアプリにする設定です。

起動しました。何も起きません。エラーも出ません。ただ黙って止まる。

原因は、許可を求めるダイアログが前面に出てこないことでした。裏方アプリなので、macOS がダイアログを前に持ってこない。ユーザーは何が起きているか分からず、アプリは永遠に応答を待ち続ける。

false に戻したら、普通にダイアログが出ました。

壁⑤ ビルドし直すと、許可が消える

これが一番きつかった。

コードを1文字直してビルドし直すと、画面収録も音声認識も、許可が全部外れます。

理由は署名です。このMacには署名用の証明書がありません。

$ security find-identity -v -p codesigning
     0 valid identities found

証明書がないとアドホック署名になります。これは中身から指紋を作る方式なので、ビルドするたびに指紋が変わり、macOS が「別のアプリ」と判断して許可を破棄します。

つまり、直す → ビルド → 許可を取り直す、の無限ループです。1日に2回はまりました。

根本的には自己署名証明書を作れば解決します。ただ、その日はそこまで手が回らなかったので、調整しそうな値を全部起動引数に逃がしました。

--silence-gap 1.1      文の区切りとみなす無音の長さ
--speech-floor -50     声とみなす音量
--locale en-US         相手の言語

作り直さずに調整できるようにする。 対症療法ですが、これで止まらなくなりました。

動いたあとに出た、本当の問題

5枚の壁を越えて、音が取れて、文字にもなりました。

なのに、翻訳が出ない。

音は認識している。画面に英語も流れている。でも日本語にならない。

原因は、文の区切りでした。

音声認識は、喋っている途中の状態を細かく返してきます。それを「いつ1文として確定させるか」を自分で決めないといけない。最初は「無音が続いたら」という判定にしていました。

会議は間があるので、これで足りると思っていました。足りませんでした。相手が喋り続けると、いつまでも確定しない。翻訳に渡す単位が決まらない。

しかも無理に切ると、こういうものが出てきます。

a dozen all king the

これを翻訳すると「ダースオールキング」になります。壊れた入力からは、壊れた訳しか出てきません。

直したのは、文末を見るという一点でした。

// 認識器は句読点を付けてくれる。それを区切りの合図に使う。
const ENDS_SENTENCE = /[.!?。!?]["')\]]?\s*$/;
if (ENDS_SENTENCE.test(fresh) && !tooShort(fresh)) commit();

間だけで判断していた頃は、話し終えてから1.3秒待たされていました。文末で即座に切るようにしたら、待ち時間が消えました。

[123ms] Could you send us a quotation by Friday
        → 金曜日までに見積書を送っていただけますか

[492ms] We also need to confirm the delivery timeline before we sign
        → 契約に署名する前に納期を確認する必要もあります

速度の問題だと思っていたものが、区切り方の問題でした。

同じ文が2回出た

もうひとつ。確定した文が、次の確定にも丸ごと混ざって出てくることがありました。

「どこまで確定済みか」を文字数で覚えていたのが原因です。音声認識は、後から前の語を直します。句読点が付いたり、大文字になったり。すると前方一致が崩れて、「全部が新しい文だ」と誤判定する。

単語数で持つように変えたら、直りました。

// 文字列の前方一致で見ていたら、同じ発言が2回出た。
// 認識器は後から前の語を直すので、単語数で持つ。
if (words.length < SEG.done) SEG.done = 0;
const fresh = words.slice(SEG.done).join(' ');

こういうのは、動かしてみるまで分かりませんでした。

一番ヒヤリとしたこと

技術の話ではありません。

自分の日本語を英語にする機能をテストしていたときです。

入力:できます。ただ、納期については一度社内で確認させてください。
訳 :However, please confirm the delivery date within the company.

読めますか。

「こちらが社内で確認します」が、「そちらが社内で確認してください」になっています。

日本語は主語を省きます。機械翻訳はそれを勝手に補うので、誰の行為かがひっくり返る。 これを商談で送っていたら、話が完全にすれ違っていました。

対策として、生成した英文をもう一度日本語に戻して、並べて表示するようにしました。

ENGLISH
We can accommodate you. However, we will confirm the delivery date
within our company.

日本語に戻すと:対応させていただきます。但し、納期につきましては
弊社にて確認させていただきます。

送る前に、自分で検算できる。

ちなみに主語を入れて「当社内で確認いたします」と書いたら、一発で正しく訳されました。AIの問題ではなく、入力の問題でした。

できたもの

core/MeetingCore.swift    356行   音声の取り込みと文字起こし
app/server.mjs            411行   区切り・翻訳・配信
app/ui.html               350行   画面

合計およそ1,100行。npm の依存パッケージは0個です。

環境は macOS 15.7、Intel の MacBook Pro。Xcode は入れていません(CommandLineTools だけでビルドできました)。

文字起こしは macOS 内蔵なので、追加費用はかかりません。英語と日本語は端末内で完結します。

画面はこうなっています。左に相手の発言と日本語訳、右に自分の返答と、その英語・ポルトガル語。上から下へ会話として読めます。

これから

正直に書いておくと、これは自分が困ったから作ったもので、商品として作ったものではありません。

いまの翻訳部分は、正式なAPIを使っていません。自分ひとりで使う分には問題ありませんが、人に渡すものにするなら、ここは正規のAPIに差し替える必要があると思っています。設計は差し替えられる形にしてあるので、作業自体は大きくありません。

もうひとつ。ポルトガル語の音声認識は端末内で完結せず、外に出ます。英語と日本語は出ません。自分の商談なら自分で判断すればいい話ですが、他社の商談を扱うものとして渡すなら、ここは説明しないといけない部分です。

そのうえで。

同じ困り方をしている方は、思ったより多いのかもしれない、とは感じています。海外とやり取りしていて、通訳を挟むほどではないけれど、相手の一言は確実に取りたい。そういう場面です。

将来的には、きちんと作り直して人にお渡しできる形にできたら面白いな、とは思っています。ただ現状は個人用の道具で、そのまま渡せるものではありません。API連携のところを含めて、見直すことがまだいくつもあります。

もし同じことで困っている方がいたら、コメントで教えてください。どこから手をつけるかの参考にします。

おわりに

エンジニアではない人間が、こういうものを作れる時代になりました。

ただ今回いちばん時間を食ったのは、コードを書くところではありませんでした。OSの都合と、それをどう回避するかの判断です。そこは AI に丸投げできませんでした。エラーメッセージが嘘をつく(「Info.plistに書け」と言われて、書いてあるのに落ちる)場面もありました。

そして最後にヒヤリとしたのは、翻訳が意味を反転させたことでした。動くかどうかより、間違ったまま動くほうが怖い。

使ってみて、はっきりしたことがあります。これは通訳の代わりにはなりません。相手の言葉には、機械が拾えない温度があります。言い淀みや、言い換えたときの間。そこに一番大事なことが入っていたりする。

結局これは、記録を残して、あとから自分の理解を確かめるための道具でした。走るのは自分で、外れかけたときに支えてくれるもの。最初に補助輪と書いたのは、そういう意味です。

同じようなものを作ろうとしている方の、時間の節約になれば嬉しいです。特に壁⑤(ビルドすると許可が消える)は、知らないと確実に溶かします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?