掲示板<37>2026/2/15
from=TOZ

Pythonを利用⑬「ai_recorder.py」

大枚を叩いて購入したcopilot-PC(Snapdragon X Elite)を使って、文字起こし pyプログラムを作った。4時間の講演内容の文字起こしをするのに、40分ぐらいか かるので、クラウドのAIを利用しないで、このパソコン内蔵の高速高性能のCPUを利 用することにした。 ホームページのタグの関係で、プログラム中の「<」「>」は全角表示しています。
import tkinter as tk
from tkinter import scrolledtext, filedialog, messagebox
import sounddevice as sd
import numpy as np
from scipy.io.wavfile import write
import threading
import whisper
import subprocess
import os
import datetime
import sys
import io

# 設定
FS = 44100
TEMP_AUDIO = "temp_recording.wav"
MODEL_NAME = "base"
AI_MODEL = "gemma2:2b"

class AIRecorderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("AI 文字起こしレコーダー (日本語要約強化版)")
        self.root.geometry("800x900")

        self.is_recording = False
        self.frames = []
        self.stream = None

        # ---------------- UI作成 ----------------
        tk.Label(root, text="AI Voice Note", font=("Arial", 16, "bold")).pack(pady=10)

        # ステータス表示
        self.status_var = tk.StringVar()
        self.status_var.set("待機中 - 準備完了")
        self.status_lbl = tk.Label(root, textvariable=self.status_var, fg="blue", font=("Arial", 10))
        self.status_lbl.pack()

        # ボタンフレーム
        btn_frame = tk.Frame(root)
        btn_frame.pack(pady=10)

        self.start_btn = tk.Button(btn_frame, text="🔴 録音開始", command=self.start_recording, 
                                   bg="#ffdddd", width=15, height=2)
        self.start_btn.pack(side=tk.LEFT, padx=10)

        self.stop_btn = tk.Button(btn_frame, text="⬛ 終了して解析", command=self.stop_recording, 
                                  state=tk.DISABLED, bg="#ddddff", width=15, height=2)
        self.stop_btn.pack(side=tk.LEFT, padx=10)

        self.file_btn = tk.Button(btn_frame, text="📂 ファイル選択\n(動画/音声)", command=self.load_file, 
                                  bg="#eebbff", width=15, height=2)
        self.file_btn.pack(side=tk.LEFT, padx=10)

        # エリア1: 文字起こし結果
        tk.Label(root, text="📝 文字起こし結果 (完了後に表示)", font=("Arial", 10, "bold")).pack(anchor="w", padx=20, pady=(10,0))
        self.transcript_area = scrolledtext.ScrolledText(root, height=6, font=("Arial", 10))
        self.transcript_area.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)

        # エリア2: AI修正結果
        tk.Label(root, text="✨ AI要約・修正結果 (完了後に表示)", font=("Arial", 10, "bold")).pack(anchor="w", padx=20, pady=(10,0))
        self.refined_area = scrolledtext.ScrolledText(root, height=8, font=("Arial", 10), bg="#f9f9ff")
        self.refined_area.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)

        # エリア3: 進捗ログ
        tk.Label(root, text="💻 処理ログ (リアルタイム進捗)", font=("Arial", 9)).pack(anchor="w", padx=20, pady=(5,0))
        self.log_area = scrolledtext.ScrolledText(root, height=8, font=("Consolas", 9), bg="#222222", fg="#00ff00")
        self.log_area.pack(fill=tk.BOTH, expand=True, padx=20, pady=5)

    def log(self, text):
        self.root.after(0, lambda: self.log_area.insert(tk.END, str(text) + "\n"))
        self.root.after(0, lambda: self.log_area.see(tk.END))

    def start_recording(self):
        self.is_recording = True
        self.frames = []
        self.start_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)
        self.file_btn.config(state=tk.DISABLED)
        self.status_var.set("🎙️ 録音中...")
        self.clear_text()
        self.log(">>> 録音開始")

        def callback(indata, frames, time, status):
            if self.is_recording:
                self.frames.append(indata.copy())

        self.stream = sd.InputStream(samplerate=FS, channels=1, callback=callback)
        self.stream.start()

    def stop_recording(self):
        if not self.is_recording: return
        self.is_recording = False
        self.stream.stop()
        self.stream.close()
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)
        self.file_btn.config(state=tk.NORMAL)
        self.status_var.set("💾 保存して解析開始...")
        self.log(">>> 録音終了。解析へ移行します。")
        threading.Thread(target=self.save_and_process).start()

    def save_and_process(self):
        if len(self.frames) == 0: return
        full_data = np.concatenate(self.frames, axis=0)
        write(TEMP_AUDIO, FS, full_data)
        self.process_audio(TEMP_AUDIO)

    def load_file(self):
        file_path = filedialog.askopenfilename(
            filetypes=[
                ("Media Files", "*.wav *.mp3 *.m4a *.flac *.mp4 *.mov *.mkv *.webm"), 
                ("Audio", "*.wav *.mp3 *.m4a *.flac"),
                ("Video", "*.mp4 *.mov *.mkv *.webm"),
                ("All", "*.*")
            ]
        )
        if not file_path: return
        self.clear_text()
        self.status_var.set(f"📂 解析準備中: {os.path.basename(file_path)}")
        self.log(f">>> ファイル選択: {file_path}")
        threading.Thread(target=self.process_audio, args=(file_path,)).start()

    def process_audio(self, audio_path):
        try:
            # --- 1. Whisper 文字起こし ---
            self.update_status("🔄 Whisper 文字起こし中...")
            self.log("--- Whisper 開始 ---")
            
            model = whisper.load_model(MODEL_NAME)
            self.log(">>> モデルロード完了")

            class StdoutRedirector:
                def __init__(self, callback): self.callback = callback
                def write(self, text): 
                    if text.strip(): self.callback(text.strip())
                def flush(self): pass

            # Whisperのログを画面に出す
            original_stdout = sys.stdout
            sys.stdout = StdoutRedirector(self.log)

            result = model.transcribe(audio_path, language="ja", fp16=False, verbose=True)
            
            sys.stdout = original_stdout # 標準出力を戻す
            transcript_text = result["text"]
            
            self.root.after(0, lambda: self.transcript_area.insert(tk.END, transcript_text))
            self.log(">>> 文字起こし完了")

            # --- 2. AI修正 (Ollama) ---
            self.update_status(f"🤖 AI({AI_MODEL}) 要約中...")
            self.log("--- AI修正開始 (日本語指定) ---")

            # プロンプトを日本語強制に強化
            prompt = f"""
            【重要】必ず「日本語」で出力してください。英語は使わないでください。 Output must be in Japanese.

            あなたは優秀な日本の編集者です。以下のテキストは講演会の音声認識結果です。
            内容を分析し、以下の構成で読みやすい日本語のレポートを作成してください。

            1. 【概要】(全体の要約を200文字程度で)
            2. 【重要なポイント】(重要な発言やトピックを箇条書きで)
            3. 【詳細な要約】(話の流れに沿って、誤字脱字を直し、読みやすく整えた文章)

            対象テキスト:
            {transcript_text}
            """

            # 標準入力経由で渡す(文字数制限対策済み)
            process = subprocess.run(
                ["ollama", "run", AI_MODEL],
                input=prompt,
                capture_output=True, text=True, encoding="utf-8"
            )

            if process.returncode == 0:
                refined_text = process.stdout
                self.root.after(0, lambda: self.refined_area.insert(tk.END, refined_text))
                
                # 自動保存
                now_str = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
                save_filename = f"memo_{now_str}.txt"
                with open(save_filename, "w", encoding="utf-8") as f:
                    f.write(f"作成日時: {now_str}\n")
                    f.write("=" * 40 + "\n")
                    f.write("【AI修正・要約結果】\n")
                    f.write(refined_text + "\n")
                    f.write("=" * 40 + "\n")
                    f.write("【元の文字起こし】\n")
                    f.write(transcript_text + "\n")

                self.update_status(f"✅ 保存完了: {save_filename}")
                self.log(f">>> 全処理完了。保存: {save_filename}")
            else:
                self.log(f"!!! AIエラー: {process.stderr}")

        except Exception as e:
            self.update_status("❌ エラー発生")
            self.log(f"!!! 例外: {e}")
            sys.stdout = sys.__stdout__

    def clear_text(self):
        self.transcript_area.delete(1.0, tk.END)
        self.refined_area.delete(1.0, tk.END)
        self.log_area.delete(1.0, tk.END)

    def update_status(self, text):
        self.root.after(0, lambda: self.status_var.set(text))

if __name__ == "__main__":
    try: subprocess.Popen(["ollama", "serve"], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
    except: pass
    root = tk.Tk()
    app = AIRecorderApp(root)
    root.mainloop()


← 一覧へ戻る