0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】睡眠×AI×天気連動の朝ルーティン最適化アプリ「Stellise」を作った。ダメ出し歓迎

0
Posted at

はじめに

「あと5分だけ…」の二度寝からの遅刻、誰もが経験あると思います。

既存の目覚ましアプリの多くは「パズルを解かないと止められない」というストレス方式。でも本当の問題は起きた後にあります。天気が急変した、電車が遅延してる、今日に限って予定が詰まってる——何から手をつければ間に合うかが分からず、結局遅刻する。

そこで作ったのが Stellise(ステライズ) です。

💡 オンデバイスAIで睡眠を解析し、天気と交通情報を加味して「今朝やるべきこと」をリアルタイムに再計算する、プライバシー重視のiOSアプリ

朝(晴れ) 朝(雨) 夜
晴れ 雨 夜

背景は画像素材ではなく、SceneKitでリアルタイム生成しています(後述)

この記事の目的: 実装を晒して、設計・UX・コード品質について忌憚のないフィードバックをいただきたいです。「ここがダメ」「この設計は危ない」「こうしたほうがいい」など、なんでも歓迎です。

🎯 解決したい課題

起きられない → 起きた後も迷う → 遅刻する

この3段階の課題を一気通貫で解決したいと考えました。

  1. スマートアラーム: 眠りの浅いタイミングで起こす(オンデバイスML)
  2. 朝タスクの自動再計算: 天気悪化・電車遅延を検知 → AIがタスクの取捨選択と順序を再構成
  3. ストレスなし: パズルではなく「やることリスト」で自然に行動を促す

🏗 技術構成

┌─────────────────────────────────────────────┐
│  iOS App (Swift / SwiftUI / Combine)        │
│  ├── SceneKit (手続き生成の3D背景)            │
│  ├── TensorFlow Lite (YAMNet 音声分類)       │
│  ├── CoreMotion (体動検知)                    │
│  ├── CoreLocation / MapKit (経路計算)         │
│  └── StoreKit 2 / App Intents / EventKit     │
├─────────────────────────────────────────────┤
│  Backend                                     │
│  ├── Flask (PythonAnywhere) ← 本体API         │
│  ├── Vercel Serverless (Node.js) ← チャット   │
│  └── Firebase (Auth / Firestore)              │
├─────────────────────────────────────────────┤
│  外部API                                     │
│  ├── OpenWeather                              │
│  ├── Google Maps Distance Matrix              │
│  └── Gemini (タスク提案 / サポートチャット)     │
└─────────────────────────────────────────────┘

🔥 技術的にがんばったポイント

1. SceneKitで3D背景をフルプロシージャル生成

画像素材を1枚も使わず、空・太陽・月・星・雲・雨をすべてコードで生成しています。

// 月のクレーターもCGContextで手続き生成
private func createMoonCraterTexture() -> UIImage {
    let size = CGSize(width: 256, height: 256)
    UIGraphicsBeginImageContext(size)
    let ctx = UIGraphicsGetCurrentContext()!
    // ランダムな位置・サイズでクレーターを描画
    for _ in 0..<12 {
        let x = CGFloat.random(in: 30...226)
        let y = CGFloat.random(in: 30...226)
        let r = CGFloat.random(in: 8...25)
        ctx.setFillColor(UIColor(white: 0.6, alpha: 0.3).cgColor)
        ctx.fillEllipse(in: CGRect(x: x-r, y: y-r, width: r*2, height: r*2))
    }
    // ...
}
  • 時刻に応じて太陽/月が東→南中→西へ弧を描いて移動
  • 天気APIの結果で雲の密度・雨の強さが変化
  • 雨は「降雨」だけでなく「窓ガラスに伝う水滴」まで表現
  • preferredFramesPerSecond = 30でバッテリー消費を抑制

🙏 フィードバック求む: SceneKitの使い方、パフォーマンス最適化の観点で「こうしたほうがいい」があれば教えてください。

2. オンデバイスMLで睡眠解析(プライバシー重視)

マイク入力 → AVAudioEngine → PCM変換 → YAMNet (TFLite) → いびき/咳の分類
CoreMotion → 加速度センサー → 体動カウント → 睡眠深度推定

すべて端末内で完結。音声の生データをサーバに送りません。

// SoundAnalyzer.swift — スレッドセーフなTFLite推論ワーカー
final class SoundInferenceWorker: @unchecked Sendable {
    private let interpreter: Interpreter
    private let queue = DispatchQueue(label: "tflite-inference", qos: .userInitiated)
    
    func classify(audioData: Data) async throws -> [ClassificationResult] {
        return try await withCheckedThrowingContinuation { continuation in
            queue.async { [self] in
                // TFLite Interpreter はスレッドセーフでないため、
                // 専用キューでシリアライズ
                // ...
            }
        }
    }
}

3. 夜空の星を決定論的に生成(メモリ節約)

NightViewの背景の星は、SwiftUIの@Stateに座標を保持するとメモリが無駄なので、線形合同法(LCG)ベースの擬似乱数で毎フレーム同じ位置に再描画しています。

// 決定論的な擬似乱数 → 毎回同じ星空を再現
struct LCG {
    var state: UInt64
    mutating func next() -> UInt64 {
        state = state &* 6364136223846793005 &+ 1442695040888963407
        return state
    }
}

4. AppState(状態管理の中核)

AppStateがObservableObjectとして天気・タスク・交通・睡眠・アラームの全状態を一元管理しています。

自覚している問題: AppStateが62KBの巨大ファイルになっており、明らかなGod Objectです。TaskService、WeatherService、AlarmServiceなどに分割すべきと思いつつ、個人開発の速度を優先してしまいました。

🙏 フィードバック求む: SwiftUIアプリでの状態管理分割のベストプラクティス、iOS 17+の@Observableへの移行判断基準など、知見があれば教えてください。

5. ドラッグ&ドロップで朝タスクを並べ替え

SwiftUIのレイアウトシステムとドラッグジェスチャーの衝突を防ぐため、ドラッグ中は一時的なorderedTasksを保持し、リリース時にAppStateへコミットするパターンを採用しました。

// TaskListView.swift
// .global 座標系を使うことで、ビュー内部オフセット変化による
// ジェスチャーのグリッチを防止
.gesture(
    DragGesture(coordinateSpace: .global)
        .onChanged { value in
            // 一時配列を操作
            updateDragPosition(value.translation)
        }
        .onEnded { _ in
            // AppStateにコミット
            commitReorder()
        }
)

🔒 プライバシー設計

データ 処理場所 サーバ送信
音声(いびき・咳検知) 端末内(YAMNet) ❌
体動データ 端末内(CoreMotion) ❌
睡眠スコア(数値のみ) サーバ ✅(Firebase匿名認証経由)
カレンダー予定 サーバ ✅(タイトルと時刻のみ)
おおよその位置情報 サーバ ✅(天気・経路計算用)
  • 広告SDK・トラッキングSDK は一切搭載していません
  • ユーザーデータをAIモデルの学習に二次利用しません

🛡 バックエンド設計で気をつけたこと

サーバサイドでのプレミアム検証

クライアントのis_premiumフラグは一切信用しません。Firestoreのサブスクリプション状態を2日TTLでキャッシュし、サーバサイドで毎回検証しています。

# backend.py — クライアントを信用しない設計
def get_user_premium_status(uid: str) -> bool:
    """
    ⚠️ クライアントが送ってくる is_premium は絶対に使わない。
    サーバ側で Firestore のサブスク状態を検証する。
    """
    cached = _premium_cache.get(uid)
    if cached and (now - cached["checked_at"]).days < 2:
        return cached["is_premium"]
    # Firestore から直接確認
    doc = db.collection("users").document(uid).get()
    # ...

クォータのトランザクション保証

Firestoreトランザクションでアトミックにインクリメント&チェック。レースコンディションでの無料ユーザーの超過を防止しています。

AI出力のサニタイズ

LLMの出力は信用できないので、sanitize_tasks()で所要時間を1〜30分にクランプ、時刻フォーマット検証、禁止キーワードフィルタ、最大15タスクキャップを適用しています。

⚠️ 自覚している課題・改善したい点

ここから先が特にフィードバックをいただきたい部分です。

アーキテクチャ

課題 詳細
AppState の God Object 化 62KBの巨大ファイル。天気・タスク・アラーム・睡眠の責務が混在
3言語のバックエンド Python (Flask) + Node.js (Vercel) + PHP (レンタルサーバ) の3ランタイム。保守コスト高
APIバージョニング未実施 /suggest_tasks のような非バージョン付きエンドポイント。破壊的変更時に全旧バージョンが壊れる
DIの欠如 AppStateがLocationManager等を直接生成しており、ユニットテスト困難

バックエンド

課題 詳細
Vercelのレートリミットが実質無効 new Map()はコールドスタートでリセットされ、インスタンス間で共有されない
チャットエンドポイントに認証なし backend.pyはFirebase Auth必須だが、chat.jsは認証なし。URL発見でGemini APIを無断利用される
datetime.now()のTZ問題 travelエンドポイントで1箇所だけnaive datetimeを使用。サーバTZが想定と異なると誤動作
sleep logのペイロード未ホワイトリスト リクエスト全体をFirestoreに保存しており、任意フィールドを注入可能

iOS

課題 詳細
NightViewの毎秒再描画 Timer.publish(every: 1.0)で時計を駆動。睡眠中のバッテリー消費が気になる
プライバシーポリシー/利用規約リンク アプリ内はまだNotionのURLを参照。Webサイトのページに統一すべき
テストカバレッジ delete_account、認証失敗パス、クォータ境界値のテストが不足

📊 技術スタック一覧

区分 使用技術
言語・UI Swift / SwiftUI / Combine
3D・グラフィックス SceneKit(背景の手続き生成)、Canvas
機械学習(オンデバイス) TensorFlow Lite(YAMNet)
センサー・位置 CoreMotion / CoreLocation / MapKit
音声 AVFoundation / AudioToolbox
OS連携 App Intents / EventKit / UserNotifications
課金 StoreKit 2
認証・同期 Firebase (Auth / Firestore)
バックエンドAPI Flask (Python) / Vercel (Node.js)
外部API OpenWeather / Google Maps / Gemini

💬 フィードバックをください!

以下の観点で特にご意見いただけると嬉しいです。もちろんそれ以外も大歓迎です。

  1. アーキテクチャ: AppStateの分割方法、iOS 17+ @Observable への移行判断
  2. SceneKit: パフォーマンス改善、Metal移行の是非
  3. オンデバイスML: YAMNet以外の選択肢、Core ML移行のメリデメ
  4. バックエンド: 3ランタイム統合の現実解、Vercelでの適切なレートリミット実装
  5. UX: オンボーディングフロー、遅刻警告の改善案
  6. プライバシー設計: 見落としているリスク
  7. 課金設計: サブスクの価格設定、フリーミアムの境界線

App Store: https://apps.apple.com/jp/app/id6760934295
GitHub: https://github.com/yuu-0617
公式サイト: https://stelise-app.com
X (Twitter): https://x.com/yuu_tokiwai
Instagram: https://www.instagram.com/stellise_app

コメント・Issue・プルリクエスト、なんでも歓迎です。厳しめのフィードバックほど成長につながると思っているので、遠慮なくお願いします 🙇


この記事が参考になったら「LGTM」していただけると励みになります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?