[iOS] APIエラーハンドリングとリトライ処理

機能 内容
エラーハンドリング 通信失敗 / デコード失敗 を正しく処理する
ステータスコード確認 API が成功か失敗かを確認する
リトライ処理 失敗したらもう一度試す

### エラーの定義

enum APIError: Error {
    case invalidURL
    case networkError
    case serverError(Int) // ステータスコード付き
    case decodeError
}

呼び出し

import Foundation

struct WeatherResponse: Codable {
    struct CurrentWeather: Codable {
        let temperature: Double
        let windspeed: Double
    }
    let current_weather: CurrentWeather
}

class WeatherAPI {
    func fetchWeather() async throws -> WeatherResponse {
        guard let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=35.6895&longitude=139.6917&current_weather=true") else {
            throw APIError.invalidURL
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        
        // ステータスコードチェック
        if let httpResponse = response as? HTTPURLResponse,
           !(200..<300).contains(httpResponse.statusCode) {
            throw APIError.serverError(httpResponse.statusCode)
        }

        // JSON デコード
        do {
            return try JSONDecoder().decode(WeatherResponse.self, from: data)
        } catch {
            throw APIError.decodeError
        }
    }
}
func fetchWeatherWithRetry() async {
    let api = WeatherAPI()
    
    for attempt in 1...3 {
        do {
            let result = try await api.fetchWeather()
            print("成功: \(result.current_weather.temperature)°C")
            return   // 成功したら終了
        } catch APIError.serverError(let code) {
            print("サーバーエラー (\(code)) → リトライ(\(attempt))")
        } catch {
            print("その他エラー → リトライ(\(attempt))")
        }
        try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒待つ
    }

    print("3回試したが失敗しました 😢")
}
import SwiftUI

struct ContentView: View {
    @State private var temperature: String = "__"
    
    var body: some View {
        VStack(spacing: 20) {
            Text("気温: \(temperature)°C")
                .font(.title)
            
            Button("天気を取得(リトライ付き)") {
                Task {
                    await fetchWeatherUI()
                }
            }
        }
        .padding()
    }

    func fetchWeatherUI() async {
        let api = WeatherAPI()
        
        for _ in 1...3 {
            do {
                let result = try await api.fetchWeather()
                await MainActor.run {
                    temperature = String(result.current_weather.temperature)
                }
                return
            } catch {
                print("失敗 → 再試行")
                try? await Task.sleep(nanoseconds: 800_000_000)
            }
        }
        await MainActor.run {
            temperature = "取得失敗"
        }
    }
}

[Figma] プラグインの使い方

プラグインにIconifyを追加する

これで使える様になる
おおお、すげえ

プロのデザイナーが使ってる要素 なぜ見た目が洗練されるか
Auto Layout 余白と整列が自動、崩れない
Components + Variants デザインが統一される
Plugins 複雑な形やアイコンを一瞬で用意できる
Vector Editing + Boolean 独自の形が作れる

プロは素人ではなぐらいの洗練されたものを作りますね。

[LLM] Flaskで簡易 /speak APIを作成

from flask import Flask, request, send_file, jsonify
from gtts import gTTS
import os
import tempfile
from datetime import datetime

app = Flask(__name__)

@app.route('/speak', methods=['POST'])
def speak():
    """
    テキストを音声に変換するAPI
    
    リクエスト例:
    {
        "text": "こんにちは、世界",
        "lang": "ja",
        "slow": false
    }
    """
    try:
        # リクエストからJSONデータを取得
        data = request.get_json()
        
        if not data or 'text' not in data:
            return jsonify({'error': 'text パラメータが必要です'}), 400
        
        text = data['text']
        lang = data.get('lang', 'ja')  # デフォルトは日本語
        slow = data.get('slow', False)  # ゆっくり話すかどうか
        
        if not text.strip():
            return jsonify({'error': 'テキストが空です'}), 400
        
        # 一時ファイルを作成
        temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3')
        temp_filename = temp_file.name
        temp_file.close()
        
        # TTSで音声ファイルを生成
        tts = gTTS(text=text, lang=lang, slow=slow)
        tts.save(temp_filename)
        
        # 音声ファイルを返す
        response = send_file(
            temp_filename,
            mimetype='audio/mpeg',
            as_attachment=True,
            download_name=f'speech_{datetime.now().strftime("%Y%m%d_%H%M%S")}.mp3'
        )
        
        # レスポンス送信後にファイルを削除
        @response.call_on_close
        def cleanup():
            try:
                os.unlink(temp_filename)
            except Exception as e:
                print(f"一時ファイルの削除に失敗: {e}")
        
        return response
        
    except Exception as e:
        return jsonify({'error': str(e)}), 500


@app.route('/speak/languages', methods=['GET'])
def get_languages():
    """サポートされている言語のリストを返す"""
    languages = {
        'ja': '日本語',
        'en': '英語',
        'zh-cn': '中国語(簡体字)',
        'zh-tw': '中国語(繁体字)',
        'ko': '韓国語',
        'es': 'スペイン語',
        'fr': 'フランス語',
        'de': 'ドイツ語',
        'it': 'イタリア語',
        'pt': 'ポルトガル語',
    }
    return jsonify(languages)


@app.route('/health', methods=['GET'])
def health_check():
    """ヘルスチェック用エンドポイント"""
    return jsonify({'status': 'ok'})


if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

$ curl -X POST http://localhost:5000/speak \ \
-H “Content-Type: application/json” \
-d ‘{“text”:”こんにちは、世界”, “lang”:”ja”}’ \
–output speech.mp3
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18096 100 18048 100 48 34083 90 –:–:– –:–:– –:–:– 34207

なるほど。textをPOSTしていますが、gptからのレスポンスをそのまま出力しても良さそうですね。。

[LLM] Google ColabでLLaMA 系モデルを動かす

Google Colabの「ランタイムのタイプを変更」でCPUからGPUに変更する

!pip install transformers accelerate bitsandbytes sentencepiece -q
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

model_name = "mistralai/Mistral-7B-Instruct-v0.2"

# トークナイザ
tokenizer = AutoTokenizer.from_pretrained(model_name)

# モデル読み込み(8bitで軽量ロード)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    device_map="auto",
    torch_dtype=torch.float16,
    load_in_8bit=True,
)

def chat(prompt):
    tokens = tokenizer(prompt, return_tensors="pt").to("cuda")
    output = model.generate(**tokens, max_new_tokens=200)
    return tokenizer.decode(output[0], skip_special_tokens=True)

print(chat("こんにちは!あなたは何ができますか?"))

こんにちは!あなたは何ができますか?

I’m a software engineer and I’ve been learning Japanese for a few years now. I’ve been using various resources to learn, but I’ve found that one of the most effective ways to learn is through immersion. That’s why I’ve decided to create a blog where I write about my experiences learning Japanese and share resources that have helped me along the way.

I hope that by sharing my journey, I can help inspire and motivate others who are also learning Japanese. And maybe, just maybe, I can help make the learning process a little less daunting for some.

So, if you’re interested in learning Japanese or just want to follow along with my journey, please feel free to subscribe to my blog. I’ll be posting new articles regularly, and I’d love to hear your thoughts and feedback.

ありがとうございます!(Arigatou go

サーバ側でGPU用意しなくて良いのは良いですね。

Google Colab で pip の前に ! がつく理由は、「Colab のセルは Python で実行されているが、pip は Python コマンドではなくシェルコマンドだから」

Python セルの中で「これはシェルコマンドだよ」と知らせる必要があります。
そのために 先頭に ! をつける:
!pip install transformers
!apt-get update
!ls

[動画] モーショングラフィックスとは

## モーショングラフィックスとは
モーショングラフィックス(Motion Graphics)とは、文字・図形・写真・イラストなどの静止したデザイン要素に「動き」をつけて映像として表現する手法

例:
TV番組のオープニング
YouTubeのアニメーションロゴ
CMのテキスト演出
UI(アプリ画面など)の動きの紹介動画

モーショングラフィックスの基本要素
要素 説明
レイアウト 画面のどこに何を置くか
タイポグラフィ 字体・文字間・サイズ・配置の工夫
色(カラーリング) 視認性と印象を決める
トランジション 要素の出入りの動きのつなぎ方
イージング(Easing) 動きを自然に見せるための速度の緩急
リズム 音楽やテンポとの同期

特にモーショングラフィックスでは
「イージング(速度の変化)」が超重要です。
✨例:同じ移動でも…
一定速度 → 機械的で不自然
ゆっくり→速く→ゆっくり → 目に優しい・意味が伝わる

🎬 制作の流れ(基本のワークフロー)
企画・目的を決める
何を伝えたい?誰に向けて?
絵コンテ・デザインを作る
Illustrator などで静止画デザインを用意
アニメーションをつける
After Effects で動きをつける
音や効果を加える
BGM・効果音で印象アップ
書き出し
mp4 / mov / GIF など用途に応じて

よく使われるソフト
用途 ソフト名
動きをつける(メイン) Adobe After Effects
素材(イラスト・形状)作成 Adobe Illustrator / Photoshop
動画編集・音合わせ Adobe Premiere Pro / DaVinci Resolve
3D 表現(必要な場合) Blender / Cinema 4D

初心者がまず練習すべきこと
文字(テキスト)の出し方・消し方
図形が動く基本アニメーション
時間曲線(イージング)の調整
シンプルなロゴアニメーション制作
最初から複雑な作品は必要ありません。
小さな動きを美しく → これが上達の鍵です。

[LLM] LlamaIndex

LlamaIndex = RAG(検索拡張生成)を簡単に作るためのフレームワーク
ChatGPT だけでは社内データを知らないので、社内ドキュメント・Wiki・PDF・DB などを検索できるようにしてから回答する必要がある。この仕組みがRAGで、RAGを作りやすくするのがLlamaIndex。

LlamaIndex は次を自動化する

ステップ 役割
文書の取り込み PDF / Word / Google Docs / Confluence などから情報を読み込む
分割 長い文書を LLM が扱いやすいサイズに切る
ベクトル化 埋め込み(embedding)に変換
検索 質問に近い文を取り出す
回答生成 LLM に「文+質問」を渡して回答

似ているものと比較
フレームワーク 主な用途
LangChain LLMアプリ全般(チャット、エージェントなど何でも型)
LlamaIndex RAG特化(社内検索・QAに強い)

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.llms.openai import OpenAI

# LLM設定(例:GPT)
llm = OpenAI(model="gpt-4o-mini")

# PDFやtxtを読み込む
documents = SimpleDirectoryReader("docs").load_data()

# ベクトル検索用インデックスを作る
index = VectorStoreIndex.from_documents(documents)

# 質問
query_engine = index.as_query_engine(llm=llm)
response = query_engine.query("経費精算の締め日は?")

print(response)

[LLM] Hugging Face Hub

Hugging Face Hub は、LLM(大規模言語モデル)や画像モデルを扱う人が、モデル・データ・コードを共有したり、実験環境を一元管理できる「AIのGitHub」みたいな場所

### Hugging Face Hub
1. モデルを探す・使う
世界中の研究機関・企業・個人が公開したモデルが 10万以上ある

モデル 説明
LLaMA / Gemma / Mistral 最新のオープンLLM
Stable Diffusion 画像生成モデル
Whisper 音声→テキスト変換

from transformers import pipeline
qa = pipeline("question-answering", model="deepset/roberta-base-squad2")

2. 自分のモデルをアップロード・共有できる

huggingface-cli login
git clone https://huggingface.co/your-model-name

3. Spaces でWebアプリを作って公開できる
Web UIを簡単に作れるサービス。
Streamlit / Gradio が標準対応。

import gradio as gr
gr.ChatInterface(lambda msg: "You said: " + msg).launch()

4. ファインチューニングが簡単にできる
特に PEFT / LoRA と相性が良く、
巨大モデルでも VRAM 8〜16GB で微調整可能。

from peft import LoraConfig, get_peft_model

from datasets import load_dataset
ds = load_dataset("squad")

5. ベクトルDBやRAG基盤も提供している
機能 説明
Inference API モデルをクラウドで実行
Inference Endpoints 企業向けセキュア推論
Embeddings RAG用のベクトル埋め込み
Text-Generation-Inference (TGI) 高速推論サーバー

サービス 例えると できること
Hugging Face Hub GitHub モデル・データ・コード共有
Spaces Netlify / Heroku AIアプリを公開できる
Transformers フレームワーク LLMを1行で使える
PEFT / LoRA 学習技術 モデルを安くチューニング
Inference API GPUクラウド 推論をホスティング

「必要な部分だけ学習する技術」の総称が PEFT
PEFT とは?
PEFT = Parameter-Efficient Fine-Tuning

その中でも特に有名なのが LoRA
LoRA = Low-Rank Adaptation

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model

model_name = "meta-llama/Llama-3-8b"
model = AutoModelForCausalLM.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

config = LoraConfig(
    r=8,
    lora_alpha=32,
    target_modules=["q_proj","v_proj"],
)

model = get_peft_model(model, config)
model.print_trainable_parameters()

学習対象は全体ではなく数%に減る

[Django] ListView

urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('book/', views.ListBookView.as_view()),
]

views.py

from django.shortcuts import render
from django.views.generic import ListView
from .models import Book
# Create your views here.

class ListBookView(ListView):
    template_name = 'book/book_list.html'
    model = Book

models.py

from django.db import models

# Create your models here.
CATEGORY = (('business', 'ビジネス'),
            ('life', '生活'),
            ('other', 'その他'))
class Book(models.Model):
    title = models.CharField('タイトル', max_length=100)
    text = models.TextField()
    category = models.CharField(
        max_length=100,
        choices =CATEGORY,
    )

[iOS] MVVMアーキテクチャ

– Model:データ構造(ToDoアイテム)
– ViewModel:データ管理・ロジック
– View:UI(ユーザーが触れる部分)

ディレクトリ構成
MVVMToDoApp/
├── Models/
│ └── Todo.swift
├── ViewModels/
│ └── TodoViewModel.swift
└── Views/
└── ContentView.swift

### Model
Todo.swift

import Foundation

struct Todo: Identifiable, Codable {
    let id = UUID()
    var title: String
    var isCompleted: Bool = false
}

### View

//
//  ContentView.swift
//  CoreDataTest
//
//  Created by mac on 2025/10/25.
//

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = TodoViewModel()
    @State private var newTodoTitle = ""
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("新しいタスクを入力", text: $newTodoTitle)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                    
                    Button(action: {
                        viewModel.addTodo(title: newTodoTitle)
                        newTodoTitle = ""
                    }) {
                        Image(systemName: "plus.circle.fill")
                            .font(.title2)
                    }
                }
                .padding()
                
                List {
                    ForEach(viewModel.todos) { todo in
                        HStack {
                            Image(systemName: todo.isCompleted ? "checkmark.circle.fill" : "circle")
                                .foregroundColor(todo.isCompleted ? .green : .gray)
                                .onTapGesture {
                                    viewModel.toggleCompletion(for: todo)
                                }
                            Text(todo.title)
                                .strikethrough(todo.isCompleted)
                        }
                    }
                    .onDelete(perform: viewModel.deleteTodo)
                }
            }
            .navigationTitle("ToDoリスト")
        }
    }
}

### ViewModels

import Foundation

class TodoViewModel: ObservableObject {
    @Published var todos: [Todo] = []
    
    func addTodo(title: String) {
        guard !title.isEmpty else { return }
        let newTodo = Todo(title: title)
        todos.append(newTodo)
    }
    
    func toggleCompletion(for todo: Todo) {
        if let index = todos.firstIndex(where: { $0.id == todo.id }) {
            todos[index].isCompleted.toggle()
        }
    }
    
    func deleteTodo(at offsets: IndexSet) {
        todos.remove(atOffsets: offsets)
    }
}

main

import SwiftUI

@main
struct MVVMToDoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

なるほど〜