Qdrantとは何か?

Qdrant公式サイト: https://qdrant.tech/

## Qdrantとは
オープンソースのベクトルデータベース
Rust製
クライアントはPython SDK, REST API, gRPCで接続できる
Qdrant自体は文章をベクトルにする機能はない、ベクトルを比較する機能だけになる

## ベクトル検索
セマンティック検索という方法があり、ドキュメント全体の意味を考慮する
ドキュメントをベクトルで表現することをembeddingという
ベクトルとベクトルを比較することで、ドキュメントの類似性を検証することができる(コサイン類似度, ベクトルとベクトルの類似度の尺度) …
bertなどの事前学習モデルでfine tuneもできるようになってきた
総当たりでベクトルを比較すると計算量が膨大になるため、精度を犠牲にして高速化している

### ベクトル検索の手順
– ベクトルデータの準備 (ニュースコーパス)
– コーパスをディクショナリ登録(json)
– コーパスのベクトル変換(GiNZA)

– Qdrantサーバ起動(docker)
– コレクションの作成
– ドキュメントを登録
– 類似ドキュメントの検索

ベクトルデータの準備とベクトル変換のところが肝になりそう

Android: Repositoryパターン RetrofitやRoomとの接続を分離して保守性UP

class UserViewModel (private val repository: UserRepository) : ViewModel() {

    val users: LiveData<List<User>> = repository.users.asLiveData()

    fun refreshUsers() {
        val users: LiveData<List<User>> = repository.asLiveData()

        fun refreshUsers() {
            viewModelScope.launch {
                repository.fetchUsersFromApi()
            }
        }
    }
}
class UserRepository(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    val users: Flow<List<User>> = userDao.getAllUsers()

    suspend fun fetchUsersFromApi() {
        try {
            val usersFromApi = apiService.getUsers()
            userDao.insertUsers(usersFromApi)
        } catch(e: Exception) {
            // error handling
        }
    }
}
class UserRepository(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    val users: Flow<List<User>> = userDao.getAllUsers()

    suspend fun fetchUsersFromApi() {
        try {
            val usersFromApi = apiService.getUsers()
            userDao.insertUsers(usersFromApi)
        } catch(e: Exception) {
            // error handling
        }
    }
}

Pythonで話者分離して、片方の話者の発話を切り抜き

無料のブラウザツールだと、音楽などで、音声とインストラメントを分離することはできるようなのですが、二人が喋っていて、片方の音声に分離することができなかったので、Pythonで実行します。

前準備として、以下の手配が必要
1) ffmpeg, pyannote.audio のインストール
2) Hugging Faceでのaccess token発行(read)およびモデル利用のaccept
3) Hugging Faceでのaccess tokenをコードのHUGGINGFACE_TOKENに埋め込み
4) python3 speaker_separation.py の実行

import subprocess
from pyannote.audio import Pipeline
from pydub import AudioSegment
import os
from collections import defaultdict

# ===== 設定 =====
mp4_file = "video.mp4"
wav_file = "conversation.wav"
output_file = "main_speaker_only.wav"

HUGGINGFACE_TOKEN = "****"  # Hugging Face token

# ===== WAV変換 =====
if os.path.exists(wav_file):
    os.remove(wav_file)

subprocess.run([
    "ffmpeg", "-y", "-i", mp4_file,
    "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
    wav_file
], check=True)

# ===== 話者分離 =====
pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-3.1",
    use_auth_token=HUGGINGFACE_TOKEN
)

diarization = pipeline(wav_file)

# ===== 各話者の合計発話時間を計算 =====
speaker_durations = defaultdict(float)
for turn, _, speaker in diarization.itertracks(yield_label=True):
    speaker_durations[speaker] += turn.end - turn.start

# 発話時間が最も長い話者を自動選択
target_speaker = max(speaker_durations, key=speaker_durations.get)
print("選択された話者:", target_speaker)

# ===== その話者だけ抽出 =====
audio = AudioSegment.from_wav(wav_file)
speaker_segments = [
    audio[int(turn.start * 1000): int(turn.end * 1000)]
    for turn, _, speaker in diarization.itertracks(yield_label=True)
    if speaker == target_speaker
]

if speaker_segments:
    speaker_audio = sum(speaker_segments)
    speaker_audio.export(output_file, format="wav")
    print(f"✅ 保存しました: {output_file}")
else:
    print("⚠️ 対象話者の音声が見つかりませんでした")

[X-code/iOS]リスト表示と動的データ

@State private var items: [String] = ["りんご", "バナナ", "みかん"]

var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                Text("AI Chat Demo")
                    .font(.title)
                
                TextField("メッセージを入力", text: $userMessage)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding()
                
                Button("送信") {
                    sendMessage()
                }
                .padding()
                
                Text("AIの返答: \(aiReply)")
                    .padding()
                
                Divider()
                
                List {
                    ForEach(items, id:\.self) { item in
                        Text(item)
                    }
                    .onDelete(perform: deleteItem)
                    .onMove(perform: moveItem)
                }
                .frame(height: 200)
                
                Button("フルーツを追加") {
                    addItem()
                }
                .padding()
                
                Spacer()
                
                NavigationLink(destination: ImageViewPage()) {
                    Text("画像ページへ移動")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(8)
                }
                NavigationLink(destination: VideoViewPage()) {
                    Text("動画ページへ移動")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.green)
                        .cornerRadius(8)
                }
//                NavigationLink(destination: CounterPage()) {
//                    Text("カウンターページへ")
//                        .foregroundColor(.white)
//                        .padding()
//                        .background(Color.green)
//                        .cornerRadius(8)
//                }
            }
            .padding()
            .navigationTitle("チャット")
        }
    }
    
    func addItem() {
        items.append("新しいフルーツ\(items.count + 1)")
    }
    
    func deleteItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
    
    func moveItem(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }
    func sendMessage() {

viewでのforeachはある程度想像がつきますね。

Difyの短期メモリ

Difyのチャットフローで メモリをオフにした場合、プレビューで入力された「こんにちは」は 常に新しい会話セッションとして扱われます。

具体的には
メモリON
過去のやり取り(今回のセッション内の履歴)を保持
LLMは前回の「こんにちは」を覚えているので、「再度のこんにちは、どうした」など、文脈を踏まえた応答になる

メモリOFF
そのノードは前のやり取りを無視
「こんにちは」と入力しても、毎回初対面の会話のように扱われ、文脈に依存しない応答になる
つまり、オフにすると 「毎回初めて会話する状態」 と考えればOK

なるほど

著作権、権利の問題

アバターの開発する際には、著作権・肖像権・商標権・人格権などの複合的な権利問題に注意する必要があります。順を追って整理します。

1. アバターの画像・デザインに関する権利
著作権:
アバターのデザイン(イラスト・3Dモデル)は「創作物」として著作権で保護されます。
他人が作ったアバターやキャラクターを無断で使用すると著作権侵害になります。
注意点:
フリー素材・商用利用可能と書かれた素材でも、改変や商用利用条件を必ず確認する。
キャラクターが特定の企業や作品に属する場合(アニメ・ゲーム・漫画など)、二次創作の範囲での利用は基本NG。
対策:
オリジナルのアバターを自作、または権利クリア済みの素材を使用する。

2. 実在人物の肖像・声の使用
肖像権:
有名人やモデルの顔をアバターに使う場合、本人の許可が必要。
声・音声:
声優や一般人の声をサンプリングしてAI音声に使用する場合も同様に許諾が必要。
注意点:
「似せた」だけでも肖像権やパブリシティ権の侵害になる場合があります。
対策:
AI生成の顔や声を使用する場合、実在人物に似すぎないデザインにする。

3. 会話内容・生成コンテンツ
著作権侵害のリスク:
GPTや他のAIを使って生成した文章自体は原則問題ありませんが、既存作品の文章をコピーした場合はアウト。
生成AIが出力する内容が特定企業の機密情報や第三者の著作物に酷似する場合もリスクあり。
対策:
利用規約で「生成内容の責任はユーザー側」に置く場合が多い。
公序良俗や名誉毀損に反する内容の生成を防ぐフィルターやモデレーションを実装。

4. 商標権・ブランド名の使用
アバターの名前やアプリ内で使用するブランド名が、既存の商標と被ると侵害になる可能性があります。
対策:
アプリ名やキャラクター名は商標検索(特許庁J-PlatPatなど)で事前確認。

5. 利用規約・プライバシー
AIチャットはユーザー入力を学習に使う場合があります。
個人情報や機密情報を取り扱う場合は、プライバシーポリシーの明示と安全なデータ管理が必須。
⚠️ まとめ
オリジナル素材を使う(画像・音声・名前)
第三者の権利は必ず確認(著作権・肖像権・商標)
ユーザー生成コンテンツに関する責任範囲を明確化
プライバシー保護・規約整備

Difyにgoogle spread sheetに連携したい

## Google Cloud Platform (GCP) で設定
GCPでGoogle Sheets APIをenableにします。
APIの認証情報(Credential)として、サービスアカウントを作成し、JSON形式のキーファイルをダウンロード(credentials.json)

## スプレッドシートの共有設定
スプレッドシートの右上の「共有」ボタンをクリックし、先ほど作成したサービスアカウントのメールアドレスを**編集者(Editor)**として追加します。

## Node.jsでライブラリをインストール
$ npm install google-auth-library googleapis

chat-sheet.js

import fs from "fs";
import fetch from "node-fetch";
import { google } from "googleapis";

// Google Sheets APIの設定
const sheets = google.sheets({ version: "v4" });
const auth = new google.auth.GoogleAuth({
  keyFile: "./credentials.json", // ダウンロードしたキーファイルのパス
  scopes: ["https://www.googleapis.com/auth/spreadsheets"],
});
const SPREADSHEET_ID = "***"; // スプレッドシートのURLから取得できるID

const DIFY_API_KEY = "app-***";
const API_URL = "https://api.dify.ai/v1/chat-messages";

const prompts = fs
  .readFileSync("./prompts.txt", "utf8")
  .split("\n")
  .map((line) => line.trim())
  .filter(Boolean);

// Difyに問い合わせる関数
async function callDify(prompt) {
  const response = await fetch(API_URL, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${DIFY_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      inputs: {},
      query: prompt,
      response_mode: "blocking",
      conversation_id: "",
      user: "cli-user",
    }),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`API Error ${response.status}: ${err}`);
  }

  const data = await response.json();
  return data.answer || "(No answer)";
}

// スプレッドシートに結果を保存する関数
async function saveToGoogleSheets(results) {
  const client = await auth.getClient();
  google.options({ auth: client });

  const values = results.map(row => [row.prompt, row.answer, row.error]);

  const resource = {
    values: [["質問", "回答", "エラー"], ...values], // ヘッダー行を追加
  };

  try {
    await sheets.spreadsheets.values.clear({
      spreadsheetId: SPREADSHEET_ID,
      range: "Sheet1!A:C", // 既存のデータをクリア(必要に応じて)
    });
    await sheets.spreadsheets.values.update({
      spreadsheetId: SPREADSHEET_ID,
      range: "Sheet1!A1",
      valueInputOption: "RAW",
      resource,
    });
    console.log("\n=== 回答をGoogleスプレッドシートに保存しました ===");
  } catch (error) {
    console.error("スプレッドシートへの書き込みエラー:", error.message);
  }
}

// メイン処理
(async () => {
  const results = [];

  for (const [i, prompt] of prompts.entries()) {
    try {
      const answer = await callDify(prompt);
      results.push({ prompt, answer, error: "" });

      // CLIに出力
      console.log(`Q${i + 1}: ${prompt}`);
      console.log(`A${i + 1}: ${answer}\n`);
    } catch (err) {
      results.push({ prompt, answer: "", error: err.message });
      console.error(`Error for "${prompt}": ${err.message}`);
    }
  }

  // Googleスプレッドシートに保存
  await saveToGoogleSheets(results);

  // (オプション)テキストファイルにも保存
  const textOutput = results
    .map((res) => `Q: ${res.prompt}\nA: ${res.answer || res.error}\n`)
    .join("\n");
  fs.writeFileSync("results.txt", textOutput, "utf8");
  console.log("=== 回答を results.txt にも保存しました ===");
})();

ほう、なるほど

Adamとloss

Adam(アダム)は、主にディープラーニングで広く使われている最適化アルゴリズムの一種。最適化アルゴリズムは、モデルの学習において、損失関数の値を最小化するためのパラメータの更新方法を決定する役割

import torch
import torch.nn as nn
import torch.optim as optim

X = torch.randn(100, 1) * 10
y = 2 * X + 1 + torch.randn(100, 1)

class LinearRegression(nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(1, 1)

    def forward(self, x):
        return self.linear(x)

model = LinearRegression()

# 損失関数とオプティマイザの定義
# 損失関数は平均二乗誤差
criterion = nn.MSELoss()

# オプティマイザはAdam
optimizer = optim.Adam(model.parameters(), lr=0.01)

# モデルの学習ループ
num_epochs = 1000

for epoch in range(num_epochs):
    
    # 順伝播
    y_pred = model(X)

    # 損失の計算
    loss = criterion(y_pred, y)
    
    # 逆伝播と最適化
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
    if (epoch+1) % 100 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# 学習後のパラメータ表示
weight, bias = model.linear.weight.item(), model.linear.bias.item()
print(f'Learned parameters: weight = {weight:.4f}, bias = {bias:.4f}')

勾配が急な方向には学習率を大きく、勾配が緩やかな方向には学習率を小さくすることで、効率的かつ安定して学習を進めることができる