はじめに:なぜこのツールを開発したのか?
エンジニアとして日々の開発業務や技術リサーチ、海外の論文・GitHub Issue・公式ドキュメントを読んでいると、以下のような「細かな翻訳・テキスト読み取りのストレス」によく直面します。
- コピー禁止や選択しづらい英文PDF論文・スライドをその場でサッと読みたい
- ターミナルに出力された英語のビルドエラーや例外スタックトレースを瞬時に把握したい
- 海外ドキュメントを読むとき、ウィンドウを切り替えずに最前面にピン留めして原文と訳文を並べて読みたい
- 既存のスクショ翻訳アプリは起動が重くメモリを消費する、あるいはキャプチャ画像がクラウドサーバーに送信されるため社内コードや機密情報には使いづらい
これらをすべて解消するため、Swift + AppKit + Apple Vision フレームワークを用いて、**0.3秒で起動し完全ローカルOCRで動作するMac専用の超軽量翻訳ツール「Zipp」**を個人開発しました。
実際の動作デモ(15秒)
キーボードの ⌃S (Control + S) を押すと即座に画面キャプチャ選択モードに入り、英語のテキスト範囲をドラッグで囲むだけで、0.3秒後にローカルOCRと翻訳が完了してフロート表示されます。
技術的な設計と実装のポイント
1. Apple Vision フレームワークによる 100% ローカル OCR 実装
スクショ翻訳ツールを開発するにあたり、「画面のキャプチャ画像が外部サーバーにアップロードされないこと」を最重要要件としました。
macOS 標準の Vision.framework (VNRecognizeTextRequest) を使用することで、すべてのテキスト認識処理をローカルの Neural Engine / CPU / GPU 内で完結させています。
import Foundation
import Vision
import AppKit
final class VisionOCRService {
/// 画面キャプチャ画像からローカルOCRを実行する
func recognizeText(from image: CGImage) async throws -> String {
let request = VNRecognizeTextRequest()
// 精度重視モード(MacのNeural Engineを活用)
request.recognitionLevel = .accurate
request.recognitionLanguages = ["en-US", "ja-JP"]
request.usesLanguageCorrection = true
let handler = VNImageRequestHandler(cgImage: image, options: [:])
try handler.perform([request])
guard let observations = request.results else { return "" }
let rawLines = observations.compactMap { $0.topCandidates(1).first?.string }
return sanitizeExtractedText(rawLines.joined(separator: "\n"))
}
}
2. PDF特有の「中途半端な改行」を自動補正するプリプロセッサ
PDF論文やターミナルのログをそのままOCRにかけると、文の途中に余計な改行(Line Break)が入ってしまい、翻訳エンジンが文脈を取り違えて誤訳を起こす問題があります。
そこで、行末がピリオドやコロンでない場合に自動でスペース結合するテキストサニタイズ処理を実装しました。
extension VisionOCRService {
/// PDFやカラム表示特有の不要な改行をマージする
private func sanitizeExtractedText(_ text: String) -> String {
let lines = text.components(separatedBy: .newlines)
var merged: [String] = []
var currentParagraph = ""
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else {
if !currentParagraph.isEmpty {
merged.append(currentParagraph)
currentParagraph = ""
}
continue
}
if currentParagraph.isEmpty {
currentParagraph = trimmed
} else if trimmed.hasPrefix("-") || trimmed.hasPrefix("•") || trimmed.hasPrefix("*") {
// リスト項目の場合は改行を維持
merged.append(currentParagraph)
currentParagraph = trimmed
} else {
// 英文の途中の改行はスペースで連結
currentParagraph += " " + trimmed
}
}
if !currentParagraph.isEmpty {
merged.append(currentParagraph)
}
return merged.joined(separator: "\n\n")
}
}
3. 最前面ピン留め(Always on Top)と非同期フローティングパネル
ブラウザやPDFリーダーをスクロールしながら参照できるように、AppKit の NSPanel (.floating レベル) を採用しています。
import AppKit
final class FloatingTranslationPanel: NSPanel {
init() {
super.init(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 280),
styleMask: [.nonactivatingPanel, .titled, .closable, .resizable],
backing: .buffered,
defer: false
)
self.level = .floating // 最前面フロート
self.isFloatingPanel = true
self.hidesOnDeactivate = false // 固定時は他アプリ操作中も隠れない
self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
}
}
30秒初期セットアップ&使い方
ショートカット操作
-
スクショOCR翻訳:
⌃S(Control + S) -
選択テキスト翻訳:
⌥G(Option + G) - 初回のみ、macOS の「システム設定 > プライバシーとセキュリティ > 画面収録」でアプリの権限をONにしてください(※画面上の選択範囲をローカルOCRで認識するためにのみ使用されます)。
おわりに
「毎日何十回も使う小さなユーティリティだからこそ、メモリ消費を極限まで抑え、0.3秒でサクッと動くことに徹底的にこだわる」という思想でSwiftネイティブ開発を行いました。
詳しい仕様やデモ動画、最新のアップデート情報は X(@dayu510)にて発信していますので、興味のある方はぜひお気軽にフィードバックをいただけると嬉しいです!


