LLM」カテゴリーアーカイブ

ローカルLLM(Ollama)とVOICEVOXで作る、英日翻訳&読み上げデスクトップアプリ

Macで動くローカルLLM環境を活かして、「英語テキストを読み込んで日本語に翻訳し、結果を表示しながら音声で読み上げる」ツールを作ってみました。さらに会話を重ねる中で、Ollamaのモデル一覧からの選択、VOICEVOXのインストール手順確認、BlackHole経由の音声入力、録音中の逐次文字起こし表示まで、機能を段階的に拡張していきました。この記事ではその過程と最終的なソースコード全体をまとめます。

前提となる環境

  • Mac mini (Apple Silicon)
  • ローカルLLM実行環境として Ollama(30B程度までのモデルを想定)
  • 日本語音声合成エンジンとして VOICEVOX
  • 音声入力用の仮想オーディオデバイスとして BlackHole

1. まず全体構成を整理する

最初の相談は「Mac上のローカルLLMを使って、英語テキストファイルを日本語に翻訳し、結果を表示しつつ音声で読み上げるには何が必要か」というものでした。整理すると、必要な要素は次の4つに分解できます。

  1. 翻訳エンジン: Ollama + 多言語対応モデル(Qwen2.5-32B-Instruct、Gemma2-27B-itなど)
  2. ファイル読み込み&パイプライン制御: Pythonでテキストを読み込み、Ollama APIに投げる
  3. 結果表示: ターミナルでも良いが、GUIならStreamlit/GradioやPyQt6が候補
  4. 音声読み上げ: macOS標準のsayコマンド、またはより高品質な日本語特化TTSであるVOICEVOX

全体のデータフローは次のようになります。

テキストファイル
   → Python(チャンク分割)
   → Ollama API(翻訳: Qwen2.5-32B等)
   → 表示(ターミナル or GUI)
   → VOICEVOX API(音声合成)
   → afplay(再生)

2. PyQt6でGUIアプリの土台を作る

方向性が固まったところで、実際にPyQt6ベースのGUIアプリとして実装することにしました。要件は次の4点です。

  • ファイル選択ボタン+読み込みボタン
  • 翻訳ボタン
  • 翻訳結果のテキスト表示
  • 読み上げ開始ボタン(VOICEVOX利用)

翻訳はOllamaの/api/generateエンドポイントを、読み上げはVOICEVOXの/audio_query/synthesisという2段階のAPIを叩き、生成したwavをafplay(macOS標準コマンド)で再生する構成です。GUIをフリーズさせないよう、翻訳・音声合成はいずれもQThreadのワーカースレッドで非同期実行しています。

3. Ollamaのモデルをollama list相当で選べるようにする

最初のバージョンはモデル名をコード内に固定していましたが、「ollama listで取得した中から選択できるように」という要望を受けて、Ollamaの/api/tagsエンドポイント(ollama listと同じ情報源)を叩き、インストール済みモデルをプルダウンに動的表示する機能を追加しました。「一覧更新」ボタンで再取得もできるようにしています。

4. VOICEVOXのインストール手順

VOICEVOXは公式サイトからMac版をダウンロードし、.dmgからApplicationsフォルダにインストールします。Apple Silicon環境では初回起動時にRosettaのインストールを求められることがあります。また、開発元未検証の警告が出た場合は「システム設定」→「プライバシーとセキュリティ」から「このまま開く」を選択することで起動できます。インストール後にアプリを起動しておくと、バックグラウンドでAPIエンジン(デフォルトポート50021)が立ち上がり、GUIアプリからの読み上げリクエストを受け付けられる状態になります。

読み上げに使われるキャラクターは、GUI上の「話者」プルダウンで選択されたものです。起動時にVOICEVOXの/speakersエンドポイントから全キャラクター×スタイルの一覧を取得して表示しています。

5. BlackHole経由の音声入力機能を追加する

テキストファイルの読み込みに加えて、「MacのBlackHole経由の音声入力からテキストを生成する機能」を追加しました。構成は次の通りです。

  • sounddeviceで入力デバイス一覧を取得し、名前に”BlackHole”を含むデバイスを自動選択
  • 録音開始/停止をトグルボタンで制御し、sd.InputStreamのコールバックで音声フレームを蓄積
  • 録音停止後、ローカルのfaster-whispersmallモデル、int8量子化でCPU動作)を使って文字起こし
  • 文字起こし結果は「原文」テキストボックスに反映され、そのまま翻訳ボタンに繋がる

これにより、システム音声(動画や配信の英語音声など)をBlackHole経由でキャプチャし、テキスト化してから翻訳・読み上げまで一気通貫で行えるようになりました。あわせて、原文テキストボックスをファイル読み込み専用の読み取り専用から、文字起こし結果を手直しできる編集可能な仕様に変更しています。

6. 録音中にテキストを逐次表示する

最後の改善は「録音中にテキストを逐次表示する」というものです。faster-whisperは真のストリーミング認識には対応していないため、録音バッファを一定間隔(デフォルト5秒)で区切り、チャンクごとに個別に文字起こしして結果を順次追記表示する方式を採用しました。

  • 録音中、PARTIAL_CHUNK_INTERVAL_SEC秒ごとに未処理の音声チャンクを取り出して文字起こし
  • 結果を蓄積し、partial_resultシグナルで「これまでの全文」を原文欄に反映(カーソルは自動的に末尾へ)
  • 録音停止時には、残っている末尾チャンクも処理して最終テキストを確定

この方式にはチャンク境界で単語が分割されると認識精度が落ちるというトレードオフがありますが、待ち時間なく認識結果を確認できる体感の良さを優先しました。間隔を短くすれば反応は早くなる一方、モデルの推論負荷が増えるため、WHISPER_MODEL_SIZE(tiny/base/small/medium/large-v3)とのバランス調整が必要です。

最終的な機能一覧

  • 英語テキストファイルの選択・読み込み
  • BlackHole経由の音声入力からの逐次文字起こし(faster-whisper使用)
  • Ollamaにインストール済みのモデル一覧からの動的選択、日本語への翻訳
  • 翻訳結果の表示・編集
  • VOICEVOXの話者一覧からの選択、音声読み上げ

必要な依存パッケージ

pip install PyQt6 requests
pip install sounddevice numpy soundfile faster-whisper   # 音声入力機能用(任意)

事前に以下も準備しておく必要があります。

  • Ollama起動+翻訳用モデルのpull(例: ollama pull qwen2.5:32b
  • VOICEVOX ENGINEの起動(公式Macアプリ、デフォルトポート50021)
  • BlackHole (2ch) のインストールと、システム音声をBlackHole経由でキャプチャできるMulti-Output Deviceの設定

最終ソースコード全体

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
英語テキストファイル → Ollama(ローカルLLM)で日本語翻訳 → VOICEVOXで読み上げ
PyQt6 GUIアプリ

前提:
  - Ollama がローカルで起動しており、翻訳用モデルが1つ以上 pull 済みであること
      $ ollama pull qwen2.5:32b
      $ ollama serve   (通常はインストール時に常駐起動される)
    起動中のOllamaが持つモデル一覧 (`ollama list` 相当) はGUI上のプルダウンから選択可能
  - VOICEVOX ENGINE がローカルで起動していること (デフォルト http://localhost:50021)
      公式サイトからmacOS版アプリ or Dockerで起動しておく
  - (任意) 音声入力機能を使う場合:
      BlackHole (2ch) をインストールし、システム音声の出力先 or Multi-Output Device に
      含めておく (https://existential.audio/blackhole/)
      文字起こしはローカルのfaster-whisperを使用
  - 依存パッケージ:
      pip install PyQt6 requests
      pip install sounddevice numpy soundfile faster-whisper   # 音声入力機能用(任意)

実行:
  python translator_app.py
"""

import sys
import os
import time
import tempfile
import subprocess

import requests
from PyQt6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QTextEdit, QFileDialog, QLabel, QMessageBox,
    QComboBox, QSpinBox
)
from PyQt6.QtCore import QThread, pyqtSignal

# 音声入力(録音+文字起こし)関連は任意依存。未インストールでも他機能は動作させる。
try:
    import sounddevice as sd
    import numpy as np
    AUDIO_LIBS_AVAILABLE = True
except ImportError:
    AUDIO_LIBS_AVAILABLE = False

# ===================== 設定 =====================
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_URL = f"{OLLAMA_BASE_URL}/api/generate"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
OLLAMA_MODEL_FALLBACK = "qwen2.5:32b"   # モデル一覧取得に失敗した場合のフォールバック

VOICEVOX_BASE_URL = "http://localhost:50021"
DEFAULT_SPEAKER_ID = 1   # 四国めたん(ノーマル)。VOICEVOXの /speakers で一覧取得可能

WHISPER_MODEL_SIZE = "small"   # tiny/base/small/medium/large-v3 (精度と速度のトレードオフ)
WHISPER_LANGUAGE = "en"        # 入力音声の言語。自動判定させたい場合は None に変更
PARTIAL_CHUNK_INTERVAL_SEC = 5.0  # 録音中、何秒ごとに区切って逐次文字起こしするか
# ==================================================

_whisper_model_cache = {}


def get_whisper_model(size: str):
    """faster-whisperモデルをロード(初回のみ)。以降はキャッシュを再利用する。"""
    from faster_whisper import WhisperModel
    if size not in _whisper_model_cache:
        _whisper_model_cache[size] = WhisperModel(size, device="cpu", compute_type="int8")
    return _whisper_model_cache[size]


class TranslateWorker(QThread):
    """Ollama APIで翻訳を実行するワーカースレッド"""
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, text: str, model: str = OLLAMA_MODEL_FALLBACK):
        super().__init__()
        self.text = text
        self.model = model

    def run(self):
        if not self.model:
            self.error.emit("翻訳に使用するモデルが選択されていません。")
            return
        prompt = (
            "以下の英文を自然な日本語に翻訳してください。"
            "訳文のみを出力し、前置きや説明、注釈は一切付けないでください。\n\n"
            f"{self.text}"
        )
        try:
            resp = requests.post(
                OLLAMA_URL,
                json={
                    "model": self.model,
                    "prompt": prompt,
                    "stream": False,
                },
                timeout=600,
            )
            resp.raise_for_status()
            data = resp.json()
            translated = data.get("response", "").strip()
            if not translated:
                raise ValueError("翻訳結果が空でした。モデル名やOllamaの起動状態を確認してください。")
            self.finished.emit(translated)
        except requests.exceptions.ConnectionError:
            self.error.emit(
                "Ollamaに接続できません。`ollama serve` が起動しているか確認してください。"
            )
        except Exception as e:
            self.error.emit(str(e))


class TTSWorker(QThread):
    """VOICEVOX APIで音声合成し、afplayで再生するワーカースレッド"""
    finished = pyqtSignal()
    error = pyqtSignal(str)

    def __init__(self, text: str, speaker: int = DEFAULT_SPEAKER_ID):
        super().__init__()
        self.text = text
        self.speaker = speaker

    def run(self):
        temp_path = None
        try:
            # 1. audio_query: テキストから音声合成用パラメータを生成
            query_res = requests.post(
                f"{VOICEVOX_BASE_URL}/audio_query",
                params={"text": self.text, "speaker": self.speaker},
                timeout=60,
            )
            query_res.raise_for_status()
            query_json = query_res.json()

            # 2. synthesis: パラメータからwav音声データを生成
            synth_res = requests.post(
                f"{VOICEVOX_BASE_URL}/synthesis",
                params={"speaker": self.speaker},
                json=query_json,
                timeout=120,
            )
            synth_res.raise_for_status()
            wav_bytes = synth_res.content

            # 3. 一時ファイルに書き出してafplayで再生 (macOS標準コマンド)
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                f.write(wav_bytes)
                temp_path = f.name

            subprocess.run(["afplay", temp_path], check=True)
            self.finished.emit()

        except requests.exceptions.ConnectionError:
            self.error.emit(
                "VOICEVOX ENGINEに接続できません。アプリ/Dockerが起動しているか確認してください。"
            )
        except Exception as e:
            self.error.emit(str(e))
        finally:
            if temp_path and os.path.exists(temp_path):
                os.remove(temp_path)


class RecordTranscribeWorker(QThread):
    """
    BlackHole等の入力デバイスから音声を録音し、一定間隔ごとにチャンク単位で
    faster-whisperによる文字起こしを行って逐次表示できるワーカースレッド。
    録音の停止は stop_recording() を外部(メインスレッド)から呼んで行う。

    partial_result: チャンクごとの文字起こし結果を反映した「これまでの全文」を通知
    finished: 停止後、最後のチャンクまで処理し終えた最終テキストを通知
    """
    partial_result = pyqtSignal(str)
    finished = pyqtSignal(str)
    error = pyqtSignal(str)

    def __init__(self, device_index, channels: int = 2, samplerate: int = 44100,
                 model_size: str = WHISPER_MODEL_SIZE, language=WHISPER_LANGUAGE,
                 chunk_interval: float = PARTIAL_CHUNK_INTERVAL_SEC):
        super().__init__()
        self.device_index = device_index
        self.channels = channels
        self.samplerate = samplerate
        self.model_size = model_size
        self.language = language
        self.chunk_interval = chunk_interval
        self._frames = []
        self._processed_index = 0  # self._frames のうち、既に文字起こし済みの位置
        self._text_parts = []
        self._stream = None
        self._stop_flag = False

    def stop_recording(self):
        self._stop_flag = True

    def run(self):
        if not AUDIO_LIBS_AVAILABLE:
            self.error.emit(
                "音声入力に必要なライブラリがありません。"
                "`pip install sounddevice numpy soundfile faster-whisper` を実行してください。"
            )
            return

        try:
            def callback(indata, frames, time_info, status):
                self._frames.append(indata.copy())

            self._stream = sd.InputStream(
                device=self.device_index,
                channels=self.channels,
                samplerate=self.samplerate,
                callback=callback,
            )
            self._stream.start()

            last_chunk_time = time.time()
            while not self._stop_flag:
                self.msleep(200)
                if time.time() - last_chunk_time >= self.chunk_interval:
                    self._process_pending_chunk()
                    last_chunk_time = time.time()

            self._stream.stop()
            self._stream.close()

            # 停止後、まだ処理していない末尾の音声を最終チャンクとして処理
            self._process_pending_chunk()

            final_text = "".join(self._text_parts).strip()
            if not final_text:
                raise ValueError("文字起こし結果が空でした。録音内容/デバイスを確認してください。")

            self.finished.emit(final_text)

        except Exception as e:
            self.error.emit(str(e))

    def _process_pending_chunk(self):
        """前回処理位置以降にたまった音声フレームをまとめて文字起こしし、結果を通知する"""
        frames_snapshot = self._frames[self._processed_index:]
        self._processed_index = len(self._frames)
        if not frames_snapshot:
            return

        chunk = np.concatenate(frames_snapshot, axis=0)
        # 極端に短いチャンク(数百ms未満)は認識精度が低いためスキップし、次回に回す
        if chunk.shape[0] < int(self.samplerate * 0.3):
            self._processed_index -= len(frames_snapshot)
            return

        tmp_path = None
        try:
            import soundfile as sf
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                tmp_path = f.name
            sf.write(tmp_path, chunk, self.samplerate)

            model = get_whisper_model(self.model_size)
            segments, _info = model.transcribe(tmp_path, language=self.language)
            chunk_text = "".join(seg.text for seg in segments)

            if chunk_text:
                self._text_parts.append(chunk_text)
                self.partial_result.emit("".join(self._text_parts).strip())
        except Exception:
            # チャンク単位の失敗で録音全体を止めない。最終結果には反映されないが継続する。
            pass
        finally:
            if tmp_path and os.path.exists(tmp_path):
                os.remove(tmp_path)


class TranslatorApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("EN→JA 翻訳&読み上げ (Ollama + VOICEVOX)")
        self.resize(760, 640)
        self.selected_path = None
        self.original_text = ""
        self.speaker_map = {}  # 表示名 -> speaker_id
        self.audio_device_map = {}  # 表示名 -> device_index
        self.record_worker = None
        self.is_recording = False
        self.init_ui()
        self.load_speakers()
        self.load_ollama_models()
        self.load_audio_devices()

    # ---------------- UI構築 ----------------
    def init_ui(self):
        layout = QVBoxLayout()

        # ファイル選択行
        file_row = QHBoxLayout()
        self.file_label = QLabel("ファイル未選択")
        select_btn = QPushButton("ファイル選択")
        select_btn.clicked.connect(self.select_file)
        self.load_btn = QPushButton("読み込み")
        self.load_btn.clicked.connect(self.load_file)
        self.load_btn.setEnabled(False)
        file_row.addWidget(self.file_label, stretch=1)
        file_row.addWidget(select_btn)
        file_row.addWidget(self.load_btn)
        layout.addLayout(file_row)

        # 音声入力行 (BlackHole等から録音 → 文字起こし)
        audio_row = QHBoxLayout()
        audio_row.addWidget(QLabel("音声入力デバイス:"))
        self.audio_device_combo = QComboBox()
        audio_row.addWidget(self.audio_device_combo, stretch=1)
        self.refresh_devices_btn = QPushButton("デバイス更新")
        self.refresh_devices_btn.clicked.connect(self.load_audio_devices)
        audio_row.addWidget(self.refresh_devices_btn)
        self.record_btn = QPushButton("録音開始")
        self.record_btn.clicked.connect(self.toggle_recording)
        audio_row.addWidget(self.record_btn)
        layout.addLayout(audio_row)

        # 原文表示 (ファイル読み込み/音声文字起こし結果を表示。翻訳前に編集可)
        layout.addWidget(QLabel("原文 (英語 / 編集可):"))
        self.original_edit = QTextEdit()
        layout.addWidget(self.original_edit)

        # 翻訳ボタン行 (モデル選択付き)
        translate_row = QHBoxLayout()
        translate_row.addWidget(QLabel("モデル:"))
        self.model_combo = QComboBox()
        translate_row.addWidget(self.model_combo, stretch=1)
        self.refresh_models_btn = QPushButton("一覧更新")
        self.refresh_models_btn.clicked.connect(self.load_ollama_models)
        translate_row.addWidget(self.refresh_models_btn)
        self.translate_btn = QPushButton("翻訳開始")
        self.translate_btn.clicked.connect(self.start_translate)
        self.translate_btn.setEnabled(False)
        translate_row.addWidget(self.translate_btn)
        layout.addLayout(translate_row)

        # 訳文表示 (編集可能: 読み上げ前に手直しできる)
        layout.addWidget(QLabel("訳文 (日本語 / 読み上げ前に編集可):"))
        self.translated_edit = QTextEdit()
        layout.addWidget(self.translated_edit)

        # 読み上げ設定行
        speak_row = QHBoxLayout()
        speak_row.addWidget(QLabel("話者:"))
        self.speaker_combo = QComboBox()
        speak_row.addWidget(self.speaker_combo, stretch=1)
        self.speak_btn = QPushButton("読み上げ開始")
        self.speak_btn.clicked.connect(self.start_speak)
        self.speak_btn.setEnabled(False)
        speak_row.addWidget(self.speak_btn)
        layout.addLayout(speak_row)

        # ステータス表示
        self.status_label = QLabel("準備完了")
        layout.addWidget(self.status_label)

        self.setLayout(layout)

    # ---------------- VOICEVOX話者一覧取得 ----------------
    def load_speakers(self):
        try:
            res = requests.get(f"{VOICEVOX_BASE_URL}/speakers", timeout=10)
            res.raise_for_status()
            speakers = res.json()
            for sp in speakers:
                name = sp.get("name", "unknown")
                for style in sp.get("styles", []):
                    label = f"{name} - {style.get('name')}"
                    self.speaker_map[label] = style.get("id")
                    self.speaker_combo.addItem(label)
            if self.speaker_combo.count() == 0:
                self.speaker_combo.addItem("デフォルト話者")
                self.speaker_map["デフォルト話者"] = DEFAULT_SPEAKER_ID
        except Exception:
            # VOICEVOXが未起動でもアプリ自体は起動できるようにする
            self.speaker_combo.addItem("デフォルト話者 (VOICEVOX未接続)")
            self.speaker_map["デフォルト話者 (VOICEVOX未接続)"] = DEFAULT_SPEAKER_ID
            self.status_label.setText(
                "VOICEVOXに接続できませんでした。読み上げ時に再接続を試みます。"
            )

    # ---------------- Ollamaモデル一覧取得 (ollama list相当) ----------------
    def load_ollama_models(self):
        self.model_combo.clear()
        try:
            res = requests.get(OLLAMA_TAGS_URL, timeout=10)
            res.raise_for_status()
            data = res.json()
            models = [m.get("name") for m in data.get("models", []) if m.get("name")]
            if not models:
                raise ValueError("インストール済みのモデルが見つかりませんでした。")
            self.model_combo.addItems(models)
            self.status_label.setText(f"モデル一覧を取得しました ({len(models)}件)")
        except requests.exceptions.ConnectionError:
            self.model_combo.addItem(OLLAMA_MODEL_FALLBACK)
            self.status_label.setText(
                "Ollamaに接続できません。`ollama serve` の起動を確認して「一覧更新」を押してください。"
            )
        except Exception as e:
            self.model_combo.addItem(OLLAMA_MODEL_FALLBACK)
            self.status_label.setText(f"モデル一覧取得に失敗しました: {e}")

    # ---------------- 音声入力デバイス一覧取得 ----------------
    def load_audio_devices(self):
        self.audio_device_combo.clear()
        self.audio_device_map.clear()

        if not AUDIO_LIBS_AVAILABLE:
            self.audio_device_combo.addItem("(sounddevice未インストール)")
            self.record_btn.setEnabled(False)
            self.status_label.setText(
                "音声入力を使うには `pip install sounddevice numpy soundfile faster-whisper` が必要です。"
            )
            return

        try:
            devices = sd.query_devices()
            preferred_index = None
            for idx, dev in enumerate(devices):
                if dev.get("max_input_channels", 0) > 0:
                    label = f"[{idx}] {dev.get('name')}"
                    self.audio_device_map[label] = idx
                    self.audio_device_combo.addItem(label)
                    if "blackhole" in dev.get("name", "").lower() and preferred_index is None:
                        preferred_index = self.audio_device_combo.count() - 1

            if self.audio_device_combo.count() == 0:
                self.audio_device_combo.addItem("入力デバイスが見つかりません")
                self.record_btn.setEnabled(False)
            else:
                self.record_btn.setEnabled(True)
                if preferred_index is not None:
                    self.audio_device_combo.setCurrentIndex(preferred_index)
        except Exception as e:
            self.audio_device_combo.addItem("デバイス取得エラー")
            self.record_btn.setEnabled(False)
            self.status_label.setText(f"音声デバイス一覧の取得に失敗しました: {e}")

    # ---------------- 録音/文字起こし処理 ----------------
    def toggle_recording(self):
        if not self.is_recording:
            self.start_recording()
        else:
            self.stop_recording()

    def start_recording(self):
        label = self.audio_device_combo.currentText()
        device_index = self.audio_device_map.get(label)
        if device_index is None:
            QMessageBox.warning(self, "警告", "有効な音声入力デバイスを選択してください。")
            return

        try:
            dev_info = sd.query_devices(device_index)
            samplerate = int(dev_info.get("default_samplerate") or 44100)
            channels = min(2, dev_info.get("max_input_channels", 1)) or 1
        except Exception:
            samplerate, channels = 44100, 2

        self.is_recording = True
        self.record_btn.setText("録音停止")
        self.audio_device_combo.setEnabled(False)
        self.refresh_devices_btn.setEnabled(False)
        self.status_label.setText("録音中... (もう一度ボタンを押すと停止し、文字起こしを開始します)")

        self.record_worker = RecordTranscribeWorker(
            device_index=device_index, channels=channels, samplerate=samplerate
        )
        self.record_worker.partial_result.connect(self.on_record_partial)
        self.record_worker.finished.connect(self.on_record_done)
        self.record_worker.error.connect(self.on_record_error)
        self.record_worker.start()

    def stop_recording(self):
        if self.record_worker:
            self.record_btn.setEnabled(False)
            self.status_label.setText("最後のチャンクを文字起こし中...")
            self.record_worker.stop_recording()

    def on_record_partial(self, text_so_far: str):
        self.original_edit.setPlainText(text_so_far)
        # カーソルを末尾に移動して、追記されていく様子が見えるようにする
        cursor = self.original_edit.textCursor()
        cursor.movePosition(cursor.MoveOperation.End)
        self.original_edit.setTextCursor(cursor)
        self.status_label.setText("録音中... (逐次認識結果を表示しています)")

    def on_record_done(self, text: str):
        self.original_edit.setPlainText(text)
        self.translate_btn.setEnabled(bool(text.strip()))
        self._reset_recording_ui()
        self.status_label.setText("文字起こし完了")

    def on_record_error(self, msg: str):
        QMessageBox.critical(self, "音声入力エラー", msg)
        self._reset_recording_ui()
        self.status_label.setText("音声入力失敗")

    def _reset_recording_ui(self):
        self.is_recording = False
        self.record_btn.setText("録音開始")
        self.record_btn.setEnabled(True)
        self.audio_device_combo.setEnabled(True)
        self.refresh_devices_btn.setEnabled(True)

    # ---------------- ファイル選択/読み込み ----------------
    def select_file(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "英語テキストファイルを選択", "", "Text Files (*.txt);;All Files (*)"
        )
        if path:
            self.selected_path = path
            self.file_label.setText(os.path.basename(path))
            self.load_btn.setEnabled(True)

    def load_file(self):
        if not self.selected_path:
            QMessageBox.warning(self, "警告", "先にファイルを選択してください")
            return
        try:
            with open(self.selected_path, "r", encoding="utf-8") as f:
                self.original_text = f.read()
            self.original_edit.setPlainText(self.original_text)
            self.translate_btn.setEnabled(bool(self.original_text.strip()))
            self.status_label.setText("ファイル読み込み完了")
        except UnicodeDecodeError:
            QMessageBox.critical(
                self, "エラー",
                "UTF-8として読み込めませんでした。ファイルの文字コードを確認してください。"
            )
        except Exception as e:
            QMessageBox.critical(self, "エラー", f"ファイル読み込みに失敗しました: {e}")

    # ---------------- 翻訳処理 ----------------
    def start_translate(self):
        text = self.original_edit.toPlainText().strip()
        if not text:
            return
        model = self.model_combo.currentText().strip()
        if not model:
            QMessageBox.warning(self, "警告", "モデルが選択されていません。「一覧更新」を押してください。")
            return
        self.translate_btn.setEnabled(False)
        self.status_label.setText(
            f"翻訳中... ({model} / モデルサイズによっては数分かかります)"
        )
        self.translate_worker = TranslateWorker(text, model=model)
        self.translate_worker.finished.connect(self.on_translate_done)
        self.translate_worker.error.connect(self.on_translate_error)
        self.translate_worker.start()

    def on_translate_done(self, translated: str):
        self.translated_edit.setPlainText(translated)
        self.translate_btn.setEnabled(True)
        self.speak_btn.setEnabled(True)
        self.status_label.setText("翻訳完了")

    def on_translate_error(self, msg: str):
        QMessageBox.critical(self, "翻訳エラー", msg)
        self.translate_btn.setEnabled(True)
        self.status_label.setText("翻訳失敗")

    # ---------------- 読み上げ処理 ----------------
    def start_speak(self):
        text = self.translated_edit.toPlainText().strip()
        if not text:
            return
        speaker_label = self.speaker_combo.currentText()
        speaker_id = self.speaker_map.get(speaker_label, DEFAULT_SPEAKER_ID)

        self.speak_btn.setEnabled(False)
        self.status_label.setText("音声合成中...")
        self.tts_worker = TTSWorker(text, speaker=speaker_id)
        self.tts_worker.finished.connect(self.on_speak_done)
        self.tts_worker.error.connect(self.on_speak_error)
        self.tts_worker.start()

    def on_speak_done(self):
        self.speak_btn.setEnabled(True)
        self.status_label.setText("読み上げ完了")

    def on_speak_error(self, msg: str):
        QMessageBox.critical(self, "読み上げエラー", msg)
        self.speak_btn.setEnabled(True)
        self.status_label.setText("読み上げ失敗")


def main():
    app = QApplication(sys.argv)
    win = TranslatorApp()
    win.show()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

まとめ

「ローカルLLMで英日翻訳+読み上げ」という単純な要件から出発し、対話を重ねる中でOllamaモデルの動的選択、VOICEVOXのキャラクター選択、BlackHole経由の音声入力、逐次文字起こし表示と、段階的に機能を積み上げていきました。すべてローカル完結(Ollama+faster-whisper+VOICEVOX)で動作するため、外部APIへの依存やコストを気にせず、システム音声のリアルタイム翻訳ツールとして拡張していける土台ができたと思います。

LLM任せでマイコンを使ったシンセサイザーを作る

以下は、コンパイルが通るまでの過程で、実行して期待通りの動作になるかは、別の問題です。
LLMに対する最初の問いかけ:マイコン、小型LCD、USB 接続のMIDIキーボーを使ってアナログシンセサイザーを作るには?

LLMの回答#1

LLMの回答コード#1

実行結果#1

実行結果をLLMへ報告して得た回答

修正版回答のコード

MIDI接続の問題解決
Gemini の回答

コンパイル時の undefined reference(リンクエラー)が発生したことで、鍵盤が全く反応しなかった本当の理由が完全に判明しました。

エラーの真因は、新しいRP2040コア(5.5.0)に同梱されているAdafruit TinyUSBライブラリの初期設定において、「MIDIホスト機能(CFG_TUH_MIDI)」自体がデフォルトで無効化され、コンパイル対象から完全に除外されていたためです。 前回、ポーリング方式で tuh_midi_stream_read 関数を無理やり直接呼び出したことで、「実体が存在しない」という事実がリンクエラーとして表面化しました(コールバック方式の際は、使用されないコードとしてコンパイラに自動消去されていたため、エラーが隠蔽されていました)。

根本的に解決するには、Arduino側のコア設定ファイルを1箇所だけ書き換える必要があります。スケッチ内の #define ではすでにコンパイル済みのライブラリに反映されないため、直接ファイルを編集します。

手順1:TinyUSBライブラリの設定変更

テキストエディタ(メモ帳など)で以下のファイルを開いてください。 C:\Users\mars\AppData\Local\Arduino15\packages\rp2040\hardware\rp2040\5.5.0\libraries\Adafruit_TinyUSB_Arduino\src\arduino\ports\rp2040\tusb_config_rp2040.h

138行目付近に #define CFG_TUH_DEVICE_MAX ... のような記述があります。その直下(またはファイル内の適当な空き行)に、以下の3行を追記して上書き保存してください。

C

#ifndef CFG_TUH_MIDI #define CFG_TUH_MIDI 1 #endif

macで稼働中のollamaをPC上のOpenWebUiから接続

Macでollamaを起動

$export OLLAMA_HOST=0.0.0.0:11434
$ollama serve

PCのOpenwebui(docker)を起動

docker run -d --name mac-webui -p 3000:8080 -e OLLAMA_BASE_URL=http://192.168.40.17:11434 -e RAG_EMBEDDING_ENGINE=ollama -e RAG_EMBEDDING_MODEL=bge-m3:567m -v mac-webui:/app/backend/data ghcr.io/open-webui/open-webui:main

動作確認

2. Open WebUI設定確認
Open WebUIの管理画面で:

Settings → Admin Settings → Connections
Ollama Base URLが正しく設定されているか確認
「Test Connection」ボタンで接続テスト

RAGでアップロードしたファイルの場所 dockerを起動後に見える

3. ホストPCからのアクセス方法 エクスプローラーのアドレスバーに

\\wsl$\docker-desktop\mnt\docker-desktop-disk\data\docker\volumes\open-webui\_data\uploads\

生成AIによる短編の作成実験

生成AIツール:文章 gemeni flash,  動画 gemini veo2

プロンプト:米国は第2次世界大戦後、ロシアとの冷戦に勝利して世界の覇権を握ってきました。しかし、最近になって国内世論の分断が進み、財政面でも負債が膨らんでいます。今日米国が世界の覇権を握っている重要な要素として、国家間の貿易が米ドルで行われていることにあると思います。この米ドルがその地位を失うシナリオは?

動画生成ツール gemini veo2が生成したイメージ動画

生成された文章を音声で要約:ツール google NotebookLM(6分34秒)

生成された全文の読み上げ:ツール VOICEBOX (14分21秒)

生成された本文 「コードの波涛、揺れるドル

東京、日本橋。老舗商社の為替担当、田中健司(40歳)は、深まる秋の空を見上げていた。ここ数年、世界の金融市場には異変が起きていた。米ドルの独歩高時代は終わりを告げ、ドルは荒い波に揉まれる小舟のようだった。各国の高インフレ、際限なき債務膨張、そして「制裁ツール」としてのドルの濫用…それらが積み重なり、ドルへの信頼は静かに、だが確実に侵食されていたのだ。

健司は、目の前のパソコン画面を見つめた。国際的な取引を行う際、通貨の両替や送金は避けて通れない。そして、そのプロセスの大半は、数十年変わらない強固な仕組みの上に成り立っていた。

「国際送金といえば、かつては決まり切った手順だったな…」

健司は心の中で呟いた。例えば、日本の会社が海外の取引先にドルで支払いを行う場合。まず日本の銀行から、海外の銀行へ送金指示を出す。この時、銀行間で安全に、迅速に「支払いに関するメッセージ」をやり取りするために使われるのが、国際銀行間通信協会、通称SWIFTというネットワークだ。世界中のほぼ全ての銀行が加盟しており、まさに国際金融の「郵便局」のような役割を果たしている。

そして、肝心の「お金」そのものの移動だが、これはほとんどの場合、米ドルを介して行われる。日本の銀行にある円預金は、コルレス契約を結んだアメリカの銀行にあるドル口座で、米ドルに両替される。そのドルは、SWIFTで受け取った指示に基づき、さらに相手国の銀行がアメリカに持つドル口座へ送金される。最終的に、そのドルが現地の通貨に両替され、取引先の口座に入金されるのだ。

なぜドルなのか?それは、ドルが世界の基軸通貨だからだ。最も取引量が多く、最も信用され、最も流動性の高い通貨。世界の貿易の大部分がドル建てで行われ、各国の政府や中央銀行が外貨準備としてドルを大量に保有している。石油取引(ペトロダラー)も長くドル建てが中心だった。この「ドルを世界の銀行が保有し、ニューヨークの銀行を通じて清算する」という仕組みが、国際金融システムの根幹を成していた。そして、この仕組みを握る米国は、世界の金の流れを把握し、特定の国や組織をシステムから締め出す金融制裁という強力な武器を持つことができたのだ。SWIFTからの排除や、米国内のドル資産凍結は、国家にとって致命的な打撃となり得た。

その堅牢さが、ドルの基軸たる所以だった…はずなのに。

「田中さん、今日の取引、どうしますか?またドルが不安定で…」

部下が不安げに覗き込む。主要な取引先である東南アジアの工場から、「今後はドル以外の決済手段を検討したい」という打診が増えていた。人民元も候補には挙がるが、中国経済への不透明感や資本規制のリスクから、躊躇する企業は多かった。ユーロや円も、それぞれの経済圏を越えるには力不足だ。

「…少し様子を見よう。こんなに不安定なドルで長期契約を結ぶのは危険すぎる」

健司はモニターの隅に表示された、聞き慣れないティッカーシンボルを眺めた。”GLOB”。それは、数年前に国際的な技術者・金融専門家集団が発表した、新しい概念のデジタル通貨だった。「グローバス(Globus)」と呼ばれるそれは、特定の国家が管理するのではなく、分散型台帳技術(ブロックチェーン)を基盤とし、複数国の主要コモディティとソフトペッグすることで価値の安定を目指す、という触れ込みだった。当初は懐疑的に見ていた健司だが、最近になってこのGLOBの取引量が静かに、だが着実に増えていることに気づいていた。

一方、ワシントンD.C.。財務省のサラ・ジェンキンス次官補(50歳)は、連日の緊急会議に疲れ果てていた。ロシアへの追加制裁、某産油国への資産凍結…ドルを武器とする米国の戦略は、短期的な効果はあっても、長期的には裏目に出始めていた。ターゲットとされた国々だけでなく、彼らと取引のある第三国までが、ドルシステムから距離を置き始めていたのだ。

サラは知っていた。米国がドルを基軸通貨として維持してきた背景には、単なる経済力だけでなく、圧倒的な軍事力と、そして何よりも「信頼」があったことを。世界の国々は、自国の通貨をドルに換え、米国債を購入することで、米国経済の安定と、それによって保たれる国際秩序、そして自由に貿易できる環境への「保険料」を支払ってきたのだ。SWIFTという「郵便局」の管理権、そしてドルという「世界の共通語」の発行権は、米国に情報と影響力をもたらした。

しかし、最近の強権的な制裁発動や、国内の度重なる政治的混乱、そして制御不能な財政赤字は、その「信頼」という最も重要な基盤を揺るがしていた。

「IMFの報告書です、次官補。各国の外貨準備におけるドルの比率が、過去20年で最も低い水準に低下しています。代替として増えているのは金、そして…あの『グローバス』です」

部下の言葉に、サラは忌々しげに顔を歪めた。

「グローバス?あの胡散臭いデジタル通貨か。特定の国家の保証もない、得体の知れないものに、なぜ各国のソブリンファンドが資金を投じるんだ?」

「…データによれば、その『特定の国家の保証がない』ことこそが、今の彼らにとって魅力的なようです。米国の意向に左右されず、政治的なリスクから切り離されている。加えて、ドルやユーロのような既存の法定通貨よりもインフレ耐性がある、と見なされています。初期の設計者が公開した安定化メカニズムの透明性も、一部で評価されているようです」

サラは苛立ちを募らせた。彼らは戦おうとしている相手が見えないのだ。特定の敵国ではない。巨大な金融機関でもない。国境を持たない、分散されたコードの集合体。どうやって規制する?どうやって圧力をかける?かつてSWIFTやコルレス銀行を盾にすれば、世界のどこへでも金融的な圧力をかけられたはずだった。しかし、グローバスは、その古いシステムの外側で、P2P(ピア・ツー・ピア)で取引されることを目指している。

「馬鹿げている!通貨は国家の主権だ!匿名性の高いデジタル資産が世界の金融システムを支配するなど、断じて許されない!」

サラの言葉は、しかし、時代の流れには届かなかった。

シンガポール。グローバスの開発を主導した「ワールド・エコノミック・レジリエンス・コンソーシアム(WERC)」の共同代表、アニャ・シャルマ博士(40代)は、晴れやかな表情で記者会見に臨んでいた。

「本日、WERCは世界の主要な穀物、エネルギー、金属を扱う複数のトレーディングプラットフォームと提携し、これらの国際取引において、決済通貨としてグローバスを導入することを発表します。これは、国境や特定の国家の意向に左右されない、真にグローバルで安定した経済活動を実現するための一歩です」

会場からは大きな拍手が起こった。彼女の背後には、欧州、アジア、南米、アフリカ…様々な地域の企業や機関のロゴが並んでいた。そこには、米国の主要企業の姿はなかったが、彼らはもう、世界の全てではなかった。

「グローバスは、特定の国家が発行する通貨ではありません。したがって、いかなる国家の制裁や政治的思惑からも独立しています。その価値は透明性の高いメカニズムによって維持され、世界中の誰でも、インターネットに接続できれば利用できます。これは、地政学的なリスクから解放された、より効率的で公正なグローバル経済への扉を開くものです」

アニャの言葉は、世界中に波紋を広げた。特に、米国の制裁に苦しむ国々や、ドル高・ドル安の波に翻弄されてきた新興国、そして既存の金融システムへの不信感を持つ人々にとって、グローバスは希望の光のように見えた。それはSWIFTやコルレス銀行といった既存の枠組みに縛られない、全く新しい選択肢だった。

東京の健司は、記者会見のニュースを見て、息を呑んだ。穀物、エネルギー…それは世界の経済を動かす最も基本的な取引だ。それがグローバスに流れ始めるということは、ドルの牙城が崩壊寸前であることを意味していた。そして、それはSWIFTを始めとする既存の金融インフラそのものへの挑戦だった。

「田中さん!どうしますか?あのグローバス、市場で急騰しています!問い合わせが殺到していて…」

部下の声に、健司は立ち上がった。迷っている時間は終わった。リスクを回避するために、そして新たなビジネスチャンスを掴むために、グローバスを理解し、活用する時が来たのだ。

「対応するぞ。すぐにグローバスの取引口座を開設する手配を。それに、クライアント向けに、グローバス決済のメリットとリスクに関する緊急説明会を設定してくれ」

米ドル中心の時代は終わりを告げた。多極化する通貨に加えて、特定の国家に属さないデジタル通貨が、新たな主要プレイヤーとして登場したのだ。それは、SWIFTのような中央集権的なシステムとは異なる、分散型のネットワークを基盤としていた。

ワシントンのサラは、WERCの発表を聞き、血の気が引くのを感じた。彼らはドルという武器を避け、全く新しい戦場で戦いを挑んできたのだ。規制しようにも、WERCはスイスに本部を置く非営利団体であり、グローバスのネットワークは世界中に分散している。かつてのように、スウィフトから排除したり、ニューヨークの銀行を締め付けたりする手法は通用しない。彼らは、自分たちの喉元に、国家の主権が及ばない「コード」でできた刃を突きつけてきたのだ。

「副長官!どうしましょう?大手銀行がグローバス取引サービスの開始を発表し始めました!無視できません!」

「くっ…彼らは…彼らは世界の金融システムそのものを迂回しようとしている…!」

サラは唇を噛み締めた。ドルの覇権とは、単に強い通貨を持っていることではなかった。ドルを基盤とした金融システム、決済ネットワーク(SWIFTなど)、そしてそれらを支える法や制度、軍事力…その複合体が「覇権」だったのだ。しかし、グローバスのような分散型デジタル通貨は、そのシステムの外側で機能しようとしている。これは、国家の権力ではコントロールしにくい、新しい時代の「力」だった。

数ヶ月後。グローバスは急速に普及していた。特に国際的なサプライチェーンを持つ企業や、国境を越えた個人間の送金で利用が拡大。金の現物取引や主要コモディティ取引の一部でも標準的な決済手段となりつつあった。その価値の安定性は、ボラティリティの高い法定通貨や他の仮想通貨と比較して際立っていた。ドルを介さず、SWIFTを通さずとも、価値が世界を移動する手段が現れたのだ。

米国の経済は困難な調整期を迎えていた。インフレは収まらず、金利は高止まり。ドル資産からの資金流出は止まらず、米国債の消化は困難を極めた。かつての「桁外れの特権」は失われ、米国は自国の経済力に見合った生活水準への調整を迫られていた。国際社会における発言力も相対的に低下し、多極化、あるいはそれ以上に複雑な「無極化」した世界で、米国は新たな立ち位置を模索していた。

中国経済は、人民元の国際的地位向上を目指したが、グローバスという強力なライバルの出現により、その道のりは平坦ではなかった。欧州や他の地域通貨も、グローバスとの共存を模索し始めた。

東京の健司は、相変わらず忙しい日々を送っていた。彼のオフィスでは、ドルだけでなく、ユーロ、円、そして何よりもGLOBのチャートが大きな画面に表示されている。世界の通貨は多様化し、それぞれが複雑に絡み合っていた。グローバスは全ての取引を置き換えたわけではないが、国際決済におけるドルのシェアを大きく奪っていた。そして、それはSWIFTのような既存のインフラへの依存度も下げていた。

「田中さん、次の取引、ロシアとの間で鉄鋼の輸入です。向こうからはグローバス建てでの請求が来ていますが、よろしいですか?」

「ああ、問題ない。グローバスでの支払い準備を進めてくれ」

健司は頷いた。かつては考えられなかった取引形態だ。国家間の政治的な対立が、直接、経済取引の通貨選択に影響を与える。そして、それを回避する手段として、国家の枠を超えた通貨が使われる。

窓の外には、夕日が沈みかけている。ドルの時代の「黄昏」は、もう完全に訪れたのかもしれない。そして、ブロックチェーンというコードによって支えられる、新しい金融世界の「夜明け」。それは希望に満ちていると同時に、未知のリスクも孕んでいる。国家の管理が及ばない通貨が、どこまで安定を保てるのか?そのガバナンスは?新たなデジタル格差は生まれないか?

健司は、パソコンのキーボードに手を置いた。金融の世界は、かつてなくエキサイティングで、かつてなく危険になっていた。彼の戦いは、まだ始まったばかりだ。コードの波涛に立ち向かい、この新しい世界を生き抜いていく。それが、彼の、そして日本の、新たな使命だった

M3 UltraとDeepSeek R1 671Bのローカル性能検証

上記のビデオをNotebookLMに読ませて、次の問い合わせで得た情報

「モデルのサイズに応じたトークン/秒の値を表形式で表示して下さい。」

はい、最新Mac Studio M3 UltraでDeepSeek R1(671B)をローカル実行した際の性能について、モデルのサイズに応じたトークン/秒の値を表形式で以下に示します。

モデルサイズトークン/秒 (mlx)mac MINI m4 pro備考
1.5 billion約 230-262
7 billion約 113-11642
14 billion約 50-6222
32 billion約 29-3210.55
70 billion約 15-165 tokens/sLlamaのクアンタイズモデル
671 billion (4bit)約 13-20ggufはGPUオフロード設定により変動、mlxの方が高速な傾向

重要なポイント:

  • mlx形式のモデルは、Appleシリコンに最適化されているため、一般的にgguf形式よりも高いトークン/秒を記録しています。
  • 671 billionパラメータのモデル(4bitクアンタイズ)の場合、mlx形式でおおよそ13-20トークン/秒gguf形式ではGPUオフロードの設定により約7-17トークン/秒となっています。
  • gguf形式の671 billionパラメータモデルでは、GPUオフロードを最大に設定することで、トークン/秒が向上する傾向が見られました。ただし、ビデオRAMの使用量が上限に近づく可能性もあるため注意が必要です。
  • 一般的に、モデルのパラメータサイズが大きくなるほど、トークン/秒の値は低下する傾向にあります。

上記は、提供されたソースに基づいた情報です。特に断りのない限り、数値は動画内で示された計測値や言及に基づいています。

DGX sparkでLLMモデルのサイズ 8B,32B,40Bを実行したさい、予想される性能は?

Grokの答え

NVIDIA DGX Sparkは、NVIDIA GB10 Grace Blackwell Superchipを搭載し、最大2000億パラメータのAIモデルをローカルで処理できるデスクトップ型AIスーパーコンピューターです。128GBの統合メモリと1000 AI TOPS(FP4精度)の性能を持ち、大規模言語モデル(LLM)の推論やファインチューニングに適しています。以下に、8B、32B、40BのLLMモデルを実行した場合の予想性能を、利用可能な情報に基づいて推定します。

1. 8B(80億パラメータ)モデルの性能

  • 推論性能: 8Bモデルは比較的小規模で、DGX Sparkの128GBメモリに十分収まります。FP4精度での1000 TOPSの計算能力を考慮すると、推論速度は数十トークン/秒(tokens/second)が期待されます。類似システム(例:NVIDIA 4090 GPU)では、8Bモデルで50-100 tokens/secondが一般的であり、DGX Sparkはこれを上回るか同等の性能(おおよそ60-120 tokens/second)が見込まれます。
  • ファインチューニング: 8Bモデルはメモリ帯域幅(273 GB/s)や計算リソースの制約を受けにくく、効率的にファインチューニング可能。バッチサイズを大きく設定でき、数時間から1日程度で高品質なチューニングが完了すると予想されます。
  • 消費電力: 約200Wの低消費電力で動作し、8Bモデルでは電力効率が非常に高い。

2. 32B(320億パラメータ)モデルの性能

  • 推論性能: 32Bモデルはメモリ使用量が増加し、128GBのメモリ内で動作可能ですが、量子化(例:FP4や4-bit)が必要な場合があります。GTC 2025でのデモでは、32Bモデル(R1-32B)のLoRAファインチューニング後の推論が「十数トークン/秒」(約10-20 tokens/second)と報告されています。これは、メモリ帯域幅やモデルサイズのスケーリングによる制約が影響している可能性があります。
  • ファインチューニング: 32Bモデルのファインチューニングは可能ですが、メモリ帯域幅(273 GB/s)がボトルネックになる場合があります。デモではリアルタイムでのLoRAチューニングが示されており、数分から数時間で軽量なチューニングが完了する性能が期待されます。
  • 課題: トークン生成速度が生成量増加に伴い低下する傾向が報告されており、長時間の推論では性能がやや落ちる可能性があります。

3. 40B(400億パラメータ)モデルの性能

  • 推論性能: 40Bモデルは32Bモデルよりさらにメモリを要求しますが、DGX Sparkの128GBメモリで量子化を活用すれば動作可能。推論速度は32Bモデルより若干低下し、10-15 tokens/second程度が現実的な範囲と考えられます。これは、メモリ容量と帯域幅の制約によるものです。
  • ファインチューニング: 40Bモデルのファインチューニングは可能ですが、効率的な量子化やバッチサイズの調整が必要。32Bモデル同様、LoRAなどの軽量手法なら数時間で完了する可能性が高いが、フルファインチューニングはメモリ制約から時間がかかる(1日以上)。
  • スケーラビリティ: 2台のDGX SparkをNVIDIA Connect-Xで接続すれば、最大4050億パラメータのモデルに対応可能ですが、40Bモデル単体では1台で十分動作します。

全体的な考察と注意点

  • メモリ制約: DGX Sparkの128GBメモリのうち、ユーザーデータ用は約100GBとされており、システムが28GBを占有します。これにより、32Bや40Bモデルでは量子化(FP4や4-bit)が必須となる場合があります。
  • 比較: 32BモデルをNVIDIA 4090(24GB VRAM)で動作させる場合、量子化を強く適用してもDGX Sparkより高速(20-30 tokens/second)な場合がありますが、メモリ容量の制約でファインチューニングは困難。DGX Sparkは大容量メモリと統合ソフトウェアスタック(NVIDIA AIソフトウェア)の利点で、推論とチューニングの両方でバランスが良い。
  • 電力効率: 200Wの低消費電力は、データセンター級の性能をデスクトップで実現する上で魅力的。8Bモデルでは余裕があり、32Bや40Bでも実用的な性能を発揮。
  • 実測値の不足: 現時点で具体的なベンチマーク(例:トークン生成速度の公式数値)が公開されておらず、推定はデモや類似システムに基づいています。実際の性能はモデル構造や最適化に依存します。
モデルサイズ推論性能 (tokens/second)ファインチューニング時間メモリ制約と注意点
8B60-120数時間128GBメモリで余裕あり。高速かつ効率的。
32B10-20数分~数時間 (LoRA)量子化推奨。メモリ帯域幅がボトルネックになる可能性。
40B10-15数時間~1日量子化必須。32Bよりやや低速、メモリ制約厳しい。

: 性能は量子化(FP4/4-bit)使用時の推定値。実測値はモデルやタスクにより変動する可能性あり。

手持ちのPCで実行した結果(数値はトークン/秒)

NucBox_M7 Pro Ryzen 9 PRO 6950Hmac mini M4ProRTX3080+GTX1080
gemma3:4b15.355122.9
deepseek-r1:8b9.535.228.6
gemma3:12b24.520 – 25.9
deepseek-r1:32b 103 – 4

LLM/RAG環境の構築

実行スクリプト

PS C:\Users\mars> docker run -d -p 3000:8080 –e RAG_EMBEDDING_ENGINE=ollama -e RAG_EMBEDDING_MODEL=bge-m3:latest –add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data –name open-webui-rag –restart always ghcr.io/open-webui/open-webui:main

PS C:\Users\mars>docker run -d -p 3000:8080 -e RAG_EMBEDDING_ENGINE=ollama -e RAG_EMBEDDING_MODEL=bge-m3:latest –gpus all –add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data –name open-webui –restart always ghcr.io/open-webui/open-webui:cuda

埋め込みモデル設定

  • 設定画面 → ドキュメント(Documents)セクションで:
  • セマンティックベクトルエンジン: Ollama
  • モデル名: bge-m3:latest
  • チャンクサイズ: 1500
  • チャンクオーバーラップ: 100
  • その他パラメータはデフォルト維持13
  • **知識ベース作成**
  • 1. Workspace → Knowledge → [+ Create Knowledge Base]
  • 2. 名前と用途(例: Assistance)を設定
  • 3. ドキュメントファイル(PDF/TXT/Markdown)をドラッグ&ドロップでアップロード[2][5]
  • **質問方法**
  • 1. 新規チャット作成時に使用するLLMを選択(例: Qwen2.5系モデル)
  • 2. チャット入力欄に`#`を入力→アップロード済みドキュメントを選択
  • 3. 自然言語で質問を入力[1][5]


openwab-uiの画面のchat controlパラメータで、設定をdefaultから変更した方が良い項目と内容を教えて

推奨される変更例:

以下は、一般的なユースケースにおける例です:

ユースケースMax TokensTemperatureTop-pRepetition Penalty
短い正確な回答1000.50.81.2
長文で詳細な説明3000.70.91.1
創造的・自由な文章生成5001.01.01.0

Grokに問い合わせた結果

Open WebUIのRAG機能を利用する場合、重要なパラメータなどの要素と、その設定値は?

Open WebUIでRAG(Retrieval-Augmented Generation)機能を利用する際、効果的な動作を実現するためには、いくつかの重要なパラメータや設定要素を適切に調整する必要があります。以下に、主要な要素とその設定値について説明します。これらはOpen WebUIの管理者パネル(例: 「設定 > ドキュメント」や「ワークスペース > モデル」)で調整可能な項目に基づいています。


1. 埋め込みモデル (Embedding Model)

  • 役割: ドキュメントをベクトル化し、検索時の類似度計算に使用するモデル。
  • デフォルト: Sentence Transformers(例: all-MiniLM-L6-v2)。軽量だがパラメータ数が少ない。
  • 推奨設定値:
  • 高精度を求める場合: nomic-embed-text(137Mパラメータ)やmxbai-embed-large(334Mパラメータ)など、より大きなモデルを選択。
  • 多言語対応が必要な場合: bge-m3(多言語対応で高性能)。
  • ポイント: デフォルトモデルは軽量だが精度が不足する場合があるため、用途に応じて変更を検討。モデル変更後はドキュメントの再アップロードが必要。

2. モデル再ランキング (Model Reranking)

  • 役割: 検索結果の関連性を再評価し、より適切なドキュメントを上位に配置。
  • デフォルト: 再ランキングなし。
  • 推奨設定値:
  • 軽量モデル: cross-encoder/ms-marco-MiniLM-L-6-v2(高速だが精度は中程度)。
  • 高精度モデル: cross-encoder/ms-marco-MiniLM-L-12-v2(より正確だが処理負荷が増加)。
  • ポイント: 再ランキングを有効にすると回答の質が向上する。特に情報量が多いドキュメントを扱う場合に有効。

3. チャンクサイズ (Chunk Size)

  • 役割: ドキュメントを分割するサイズ。検索精度とコンテキストのバランスに影響。
  • デフォルト: 1500トークン。
  • 推奨設定値:
  • 小規模ドキュメント: 500〜1000トークン(細かい粒度で検索精度を向上)。
  • 大規模ドキュメント: 1500〜2000トークン(コンテキストを広く保持)。
  • ポイント: 小さすぎるとコンテキストが不足し、大きすぎるとノイズが増えるため、ドキュメントの内容に応じて調整。

4. コンテキストの長さ (Context Length)

  • 役割: LLMが回答生成時に参照するコンテキストの最大長。
  • デフォルト: 2048トークン。
  • 推奨設定値:
  • 短い回答: 1024〜2048トークン。
  • 詳細な回答: 4096トークン(モデルが対応している場合)。
  • ポイント: 使用するLLMの最大トークン数に依存。長すぎると処理速度が低下する可能性がある。

5. トップK (Top K)

  • 役割: 検索結果から上位何件のドキュメントをコンテキストとして使用するか。
  • デフォルト: 10。
  • 推奨設定値:
  • 高精度重視: 3〜5(関連性の高いものだけを厳選)。
  • 情報量重視: 10〜15(幅広い情報をカバー)。
  • ポイント: 値が大きいほど多様な情報を含むが、ノイズも増える可能性がある。

6. ドキュメントの前処理

  • 役割: 検索用データの品質を向上させる。
  • 設定値:
  • テキストファイル推奨(PDFやWordは事前にテキスト変換)。
  • ノイズ除去(余分な空白、タブ、改行の整理)。
  • ポイント: 前処理が不十分だと検索精度が落ちるため、きれいなデータを用意することが重要。

7. 生成モデル (LLM)

  • 役割: 検索されたコンテキストをもとに回答を生成するモデル。
  • 推奨設定値:
  • ローカル: gemma2:2b(軽量)、ELYZA-japanese-Llama-2-13b(日本語対応)。
  • API経由: gpt-4o-mini(高精度だがコストがかかる)。
  • ポイント: RAGの効果は生成モデルの性能にも依存。ローカル環境ではハードウェアスペックを考慮。

設定の流れ(例)

  1. 管理者パネル > 設定 > ドキュメントで埋め込みモデルをbge-m3に変更。
  2. モデル再ランキングcross-encoder/ms-marco-MiniLM-L-12-v2を指定。
  3. チャンクサイズを1000、コンテキストの長さを2048、トップKを5に設定。
  4. 前処理済みのテキストファイルをアップロード。
  5. ワークスペース > モデルで生成モデル(例: gemma2:2b)を選択し、RAG用にカスタムモデルを作成。

注意点

  • ハードウェア依存: ローカル環境ではGPUやメモリ容量が処理速度に影響。
  • 再アップロード: 埋め込みモデルやチャンクサイズを変更した場合、既存ドキュメントを削除して再登録が必要。
  • 試行錯誤: 最適な設定はデータや用途により異なるため、実験しながら調整する。

これらのパラメータを適切に設定することで、Open WebUIのRAG機能を最大限に活用し、高精度で有用な回答を得られる環境を構築できます。

100K文字程度のテキストをRAGで検索すると仮定した際に、それぞれのパラメータの最適値は

100K文字程度のテキストをRAG(Retrieval-Augmented Generation)で検索する場合、データ量としては中規模(日本語で約50K〜100Kトークン程度と仮定)に該当します。この規模のテキストを効率的かつ高精度に処理するためには、パラメータの設定に以下の点を考慮する必要があります。以下に、前回の回答をベースに具体的な最適値を提案します。


前提条件

  • テキスト量: 100K文字(日本語の場合、約50Kトークンと仮定。句読点や単語の長さに依存)。
  • 目的: 関連性の高い情報を迅速に検索し、正確な回答を生成。
  • 環境: ローカル(GPUあり)またはクラウド(API利用)を想定。

1. 埋め込みモデル (Embedding Model)

  • 最適値: bge-m3
  • 理由:
  • 100K文字は複数のトピックを含む可能性が高く、多言語対応かつ高精度な埋め込みが必要。
  • bge-m3は約568Mパラメータを持ち、日本語を含む多言語で優れた性能を発揮。
  • 代替案: リソースが限られる場合、all-MiniLM-L12-v2(軽量かつそこそこの精度)。

2. モデル再ランキング (Model Reranking)

  • 最適値: cross-encoder/ms-marco-MiniLM-L-12-v2
  • 理由:
  • 中規模テキストでは検索結果にノイズが混ざりやすいため、再ランキングで精度を向上。
  • L-12-v2は精度と速度のバランスが良く、100K文字程度の処理に適している。
  • 代替案: 高速処理優先ならMiniLM-L-6-v2

3. チャンクサイズ (Chunk Size)

  • 最適値: 800〜1000トークン
  • 理由:
  • 100K文字(約50Kトークン)を50〜60チャンクに分割可能。
  • 800〜1000トークンは、1チャンクで十分なコンテキストを保持しつつ、検索時のノイズを抑える。
  • 補足:
  • 日本語は英語よりトークン数が多くなりがちなので、1000トークン以下に抑えるのが現実的。
  • 小さすぎると(例: 500トークン)コンテキストが不足し、大きすぎると(例: 2000トークン)関連性が薄れる。

4. コンテキストの長さ (Context Length)

  • 最適値: 2048トークン
  • 理由:
  • 検索結果から複数チャンク(例: トップ3〜5)を参照する場合、合計で1500〜2000トークン程度になる。
  • 2048トークンは多くのモデル(例: Llama系やGemma系)が対応可能で、実用的な上限。
  • 補足: 高性能モデル(例: GPT-4o, 128Kトークン対応)を使う場合、4096トークンも検討可能だが、ローカルでは負荷が増大。

5. トップK (Top K)

  • 最適値: 5
  • 理由:
  • 100K文字から50〜60チャンクが生成されると仮定し、上位5件で十分な関連性を確保。
  • 5は精度と情報量のバランスが良く、生成時のコンテキスト過多を防ぐ。
  • 補足: 幅広い情報を求める場合、10まで増やしても可。ただし、ノイズが増えるリスクを考慮。

6. ドキュメントの前処理

  • 最適値:
  • テキストを事前に整形(余分な空白や改行を削除)。
  • 段落ごとに明示的な区切り(例: ---)を挿入。
  • 理由:
  • 100K文字は構造化されていない場合が多く、前処理で検索精度を向上。
  • 日本語特有の曖昧な句読点を整理することで、チャンク分割が自然に。

7. 生成モデル (LLM)

  • 最適値:
  • ローカル: gemma2:9b(9Bパラメータで高精度かつ中規模データに適応)。
  • クラウド: gpt-4o-mini(コスト効率と性能のバランス)。
  • 理由:
  • 100K文字のコンテキストを理解し、自然な日本語で回答するには、中〜大規模モデルが必要。
  • gemma2:9bはローカルで実行可能な高性能モデル、gpt-4o-miniはAPI利用時の最適解。

具体的な設定例

  • 埋め込みモデル: bge-m3
  • 再ランキング: cross-encoder/ms-marco-MiniLM-L-12-v2
  • チャンクサイズ: 1000トークン
  • コンテキストの長さ: 2048トークン
  • トップK: 5
  • 前処理: 整形済みテキスト、段落区切り付き
  • 生成モデル: gemma2:9b(ローカル)またはgpt-4o-mini(クラウド)

想定される動作

  • チャンク数: 100K文字 ÷ 1000トークン ≈ 50チャンク。
  • 検索時間: 埋め込み生成と再ランキングで数秒〜十数秒(ハードウェア依存)。
  • コンテキストサイズ: トップ5 × 1000トークン = 約5000トークン(2048に制限されるため、実質上位2〜3チャンクが使用される)。

調整のポイント

  • 精度不足の場合: トップKを3に減らし、再ランキングモデルの精度を上げる(例: L-12-v2Electra)。
  • 速度不足の場合: チャンクサイズを1200に増やし、チャンク数を減らす。
  • コンテキスト不足の場合: コンテキスト長を4096に拡張(対応モデルが必要)。

この設定なら、100K文字程度のテキストをRAGで効率的に検索し、高精度な回答を生成できるはずです。実際のデータやハードウェアに応じて微調整してください。

今年のノーベル賞はAI祭り

物理学賞2024年のノーベル物理学賞は,「人工ニューラルネットワークによる機械学習を可能にする基礎的発見と発明」の功績で,米プリンストン大学のホップフィールド(John Hopfield)名誉教授とカナダのトロント大学のヒントン(Geoffrey Hinton)名誉教授に授与される。 (日経サイエンス)

化学賞2024年のノーベル化学賞は「コンピューターを用いたタンパク質の設計」の功績で米ワシントン大学のベイカー(David Baker)教授に,「コンピューターを用いたタンパク質の構造予測」で英国Google DeepMindのハサビス(Demis Hassabis)氏とジャンパー(John Jumper)氏に授与される (日経サイエンス)

化学賞受賞のハサビス氏は、世界最高ランクの囲碁棋士である李世乭に5試合中4試合に勝利したAlphaGoプログラムを開発したほか、機械学習のスタートアップであるDeepMindテクノロジーズを共同で立ち上げCEOを務めた。

生成AIを利用した様々なサービスが利用できる昨今ですが、Googleが今のところ無料で公開しているnotebookLMを使ってみました。(料金については、Googleによると「NotebookLM はまだ早期テスト段階であるため、現時点では料金は発生しません」となっていますが、そのうちに課金の対象になるかも?)

Google NotebookLMは、ユーザがアップロードした資料に対して、要約や質問に対する回答などを得ることができます。試しに、自分が2008年に執筆した博士論文のpdf(約100ページ)をアップロードしてみました。Google NotebookLMの興味深い機能の一つに、男女2人が、アップロードしたファイルの内容についての説明・感想を数分程度の会話を音声で聞くことができます。アップロードした資料が日本語であっても、今のところ音声は英語だけなのが残念な点ですが、再生できるようにしてみました。

Google NotebookLMが処理した結果の「要約」は、次のようになりました。Google NotebookLMの出力は、飾りのないテキストなので、多少読みやすくなるよう手動で体裁を整えました。

マルウェア自動解析システムの研究

1. マルウェアの脅威と対策
 近年、マルウェアによる被害が急増しており、その種類や手法も多様化していることを説明。
従来のシグネチャベースの対策では未知のマルウェアへの対応が困難であることを指摘。
マルウェアの挙動解析に基づく自動解析システムの必要性を論述。

2. マルウェア自動解析システムの概要
 本研究で提案するマルウェア自動解析システムの全体構成を図を用いて説明。
システムの主要な機能である、マルウェアの動的解析、挙動データの収集、数値化と類似性判定、そして視覚化について概説。

3. マルウェアの動的解析
 サンドボックス環境を用いたマルウェアの動的解析手法について解説。
APIコール、レジストリへのアクセス、ネットワーク通信など、解析対象となるマルウェアの挙動を列挙。解析結果から得られる情報とその利点について説明。

4. 挙動データの数値化と類似性の判定
 マルウェアの挙動データを数値化するための特徴量抽出方法を説明。
使用する特徴量として、APIコール系列、レジストリキー、通信先ポート番号などを例示。
数値化された挙動データに基づいて、マルウェア間の類似性を判定するためのアルゴリズムを解説。具体的には、ユークリッド距離、マンハッタン距離、コサイン類似度などの距離尺度を用いた類似度計算方法を説明。

5. 挙動データの視覚化
 多次元尺度構成法 (MDS) を用いたマルウェア挙動の視覚化手法について説明。
MDS により、高次元の特徴量空間を2次元または3次元に写像することで、マルウェアの類似性を視覚的に表現。視覚化結果から、マルウェアの分類や未知マルウェアの検出が可能になることを示唆。

6. 実験と評価
 実験に用いたマルウェア検体データセットの概要を説明。
検体数は約8,000個体で、トレンドマイクロ・ウィルスバスター2007を用いてスキャンし、名前が判明したものを使用。提案システムの性能評価指標として、検出率、誤検知率、処理時間を設定。実験結果に基づいて、提案システムが従来手法と比較して高い検出率と低い誤検知率を達成したことを示す。処理時間についても、実用的な範囲内であることを確認。

7. PDBを用いたマルウェア挙動の可視化
 マルウェアの挙動を可視化するために、PDB (Position Dependent Bits) を導入。
PDBを用いることで、マルウェアの挙動をビット列として表現し、視覚的に比較分析することが可能になる。実際に、上位15種類のマルウェアについて、PDBへの変換結果を視覚化し、その特徴を分析。結果として、同種に分類されるマルウェアの挙動は一定ではないものの、利用ポートの状況など、種類ごとに特徴が認められることを示す。

8. 結論と今後の展望
 本研究では、マルウェアの挙動解析に基づく自動解析システムを提案し、その有効性を検証。
提案システムは、未知のマルウェアに対しても高い検出率を達成し、マルウェア対策への貢献が期待される。今後の課題として、解析精度の向上、処理速度の高速化、マルウェアの進化への対応などを挙げ、さらなる研究の必要性を論述。

9. 付録
 上位15種類のマルウェアのPDB可視化結果の詳細を掲載。実験対象とした検体の内訳を、AV製品ごとに「科名」と「科名+亜種名」で分類し、検体数を表示。特徴量抽出に用いたパラメータ (port100_md50_svcN_rootN_host_file4.hex) と、それによって得られたデータセットの分割方法について説明。各分割データセットにおける、F値、F&V値、F-STD、F&V-STD、Totalなどを表形式で表示。

参考文献
本研究に関連する参考文献を列挙。

LOCAL LLM ollama

pdfファイルをアップロードしてRAG(Retrieval-Augmented Generation)

参考動画 installの参考

https://www.youtube.com/watch?v=1xdneyn6zjw

Step 1: Install Ollama https://ollama.com/
Step 2: Copy and Paste Llama 3 install command using Terminal
Step 3: Add other LLM models (optional) https://ollama.com/library
Step 4: Install Docker https://www.docker.com/
Step 5: Install OpenWebUI https://docs.openwebui.com/getting-st...
Login to OpenWebUI and start using your local AI chatbot.

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
STEP1:ollamaの実行
C:\Users\ei2k-> ollama.exe run llama3.1

STEP2:docker desktop実行

STEP3:dockerでopen-webuiを実行
C:\Users\ei2k-> docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

LLM活用のプロンプト・テクニック

出典:

  • 1、孫正義式ディベートプロンプト 【〇〇な】Aさん、【〇〇な】Bさん、【〇〇な】Cさん あなたがたならこの問題をどう解決するか 私の目の前でディベートしてください 問題 【〇〇】
  • 2、AIを詰める?パワハラプロンプト ありがとうございます、この出力を60点とします これを60点とした時に100点とはどのようなものですか? 100点にするために足りないものを列挙した後に 100点の答えを生成してください
  • 3、分かりやすい回答に改善するプロンプト 【〇〇】について 人と人が対話する物語形式で 分かりやすく解説してください
  • 4、シンガポール発CO-STARフレームワーク (C) コンテキスト:【〇〇】 (O) 目標:【〇〇】 (S) スタイル:【〇〇】 (T) トーン:【〇〇】 (A) 対象読者:【〇〇】 (R) 応答:以下の形式で回答を構成してください: 1.【〇〇】 2.【〇〇】 3.【〇〇】
  • 5、段階理解プロンプト 「【〇〇】」 ↑の文言を意味を保ったまま別の言葉に言い換えてください
  • 6、プロンプトを短縮して精度向上 https://chatgpt.com/g/g-G7bqU5uCZ-lis…
  • 7、ハルシネーションを避けるプロンプト手法 【〇〇】 わからないことは「知らない」と答えてください。
  • ゴール(目的)
  • 前提(AIの役割、自分の状況)
  • 出力形式(表、json…)
  • ステップ
  • 例示

https://www.perplexity.ai/search/sekiyuriteizhuan-men-jia-asan-EjFLfr6eTEC4waiY5_ZRXQ