ponshu-room-lite/docs/archive/ANTIGRAVITY_PROMPT.md

18 KiB
Raw Blame History

Antigravity向け - 新生ぽんるーむ実装プロンプト

実装日: 2025-12-29以降 実装担当: Antigravity サポート: Claude (Anthropic)


🚨 最重要指示

絶対にやってはいけないこと

❌ 既存のWeb版ponshu-room/lib/からコードを1行もコピーしない
❌ 既存プロジェクトを編集しない
❌ Web版のUIを踏襲しない

やるべきこと

✅ 完全に新しいFlutterプロジェクトを作成
✅ FINAL_REQUIREMENTS.mdに100%従う
✅ スキャンアプリmai_quick_scanの成功パターンを参考にする
✅ 写真を主役にする

📱 プロジェクト作成(コピペして実行)

ステップ1: 新規プロジェクト作成

# 親ディレクトリへ移動
cd C:\Users\maita\posimai-project

# 完全に新しいFlutterプロジェクトを作成
flutter create ponshu_room_reborn

cd ponshu_room_reborn

ステップ2: pubspec.yaml編集

完全に置き換え:

name: ponshu_room_reborn
description: "Reborn Ponshu Room - My Digital Sake Cellar"
publish_to: 'none'
version: 2.0.0+1

environment:
  sdk: ^3.10.1

dependencies:
  flutter:
    sdk: flutter

  # 状態管理(スキャンアプリと同じ)
  flutter_riverpod: ^2.6.1
  hooks_riverpod: ^2.6.1
  flutter_hooks: ^0.20.5

  # ローカルDB
  hive: ^2.2.3
  hive_flutter: ^1.1.0

  # AI解析
  google_generative_ai: ^0.4.7
  http: ^1.6.0

  # カメラ・画像
  camera: ^0.11.0+2
  image_picker: ^1.2.1
  image: ^4.3.0

  # UI
  google_fonts: ^6.2.1
  fl_chart: ^1.1.1

  # 共有・保存
  share_plus: ^12.0.1
  path_provider: ^2.1.5

  # その他
  intl: ^0.20.2
  package_info_plus: ^9.0.0
  url_launcher: ^6.3.2
  countries_world_map: ^1.3.0
  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.0.0
  hive_generator: ^2.0.1
  build_runner: ^2.4.13

flutter:
  uses-material-design: true
flutter pub get

ステップ3: Android設定

android/app/build.gradle.kts を編集:

android {
    namespace = "com.posimai.ponshu_room"
    compileSdk = 36  // Android 15+ 対応

    defaultConfig {
        applicationId = "com.posimai.ponshu_room"
        minSdk = 21
        targetSdk = 35
        versionCode = 1
        versionName = "2.0.0"
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17.toString()
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("debug")
        }
    }
}

🎨 デザイン仕様(最重要)

写真を主役にする「ナロー・マージン」設計

従来の余白設計(ダメな例)

ListView.separated(
  padding: EdgeInsets.all(16),  // ← 余白が大きすぎ
  separatorBuilder: (context, index) => SizedBox(height: 16),  // ← 隙間が大きすぎ
  itemBuilder: (context, index) => SakeCard(...),
)

問題点: 写真が小さくなる、迫力がない

新生ぽんるーむの余白設計(正しい例)

ListView.separated(
  padding: EdgeInsets.symmetric(horizontal: 8, vertical: 12),  // ← 最小限
  separatorBuilder: (context, index) => SizedBox(height: 4),   // ← 細い隙間
  itemBuilder: (context, index) => SakeCard(...),
)

メリット: 写真が大きい、迫力がある、Instagram的

カードデザイン - 「フル幅カード」

// lib/widgets/sake_card.dart
class SakeCard extends StatelessWidget {
  final SakeItem item;

  const SakeCard({required this.item});

  @override
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;

    return Card(
      margin: EdgeInsets.zero,  // ← カード自体のマージンはゼロ
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(0),  // ← 角丸なし(フル幅)
        side: BorderSide(
          color: Color(0xFFE8E8E8),
          width: 0.5,  // ← 繊細なボーダー
        ),
      ),
      child: Container(
        decoration: BoxDecoration(
          border: Border(
            bottom: BorderSide(
              color: Color(0xFFE8E8E8),
              width: 0.5,
            ),
          ),
        ),
        child: Padding(
          padding: EdgeInsets.all(12),  // ← カード内の余白は最小限
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 左: 写真(できるだけ大きく)
              ClipRRect(
                borderRadius: BorderRadius.circular(8),  // ← 写真だけ角丸
                child: Image.network(
                  item.imagePath,
                  width: 120,  // ← 大きく!
                  height: 120,
                  fit: BoxFit.cover,
                ),
              ),
              SizedBox(width: 12),
              // 右: テキスト情報
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    // 銘柄名(明朝体、大きく)
                    Text(
                      item.brandName ?? '日本酒',
                      style: GoogleFonts.notoSerifJp(
                        fontSize: 18,
                        fontWeight: FontWeight.w600,
                        color: Color(0xFF1A1A1A),
                      ),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                    SizedBox(height: 4),
                    // 酒蔵・産地
                    Text(
                      '${item.breweryName ?? ''} | ${item.prefecture ?? ''}',
                      style: GoogleFonts.notoSansJp(
                        fontSize: 11,
                        color: Color(0xFF8A8A8A),
                      ),
                    ),
                    SizedBox(height: 8),
                    // 評価
                    if (item.rating != null)
                      Row(
                        children: List.generate(
                          5,
                          (index) => Icon(
                            index < item.rating!.round()
                                ? Icons.star
                                : Icons.star_border,
                            size: 16,
                            color: Color(0xFFFFC107),
                          ),
                        ),
                      ),
                    SizedBox(height: 4),
                    // 種類
                    if (item.type != null)
                      Text(
                        item.type!,
                        style: GoogleFonts.notoSansJp(
                          fontSize: 11,
                          color: Color(0xFF4A4A4A),
                        ),
                      ),
                    // キャッチコピー
                    if (item.catchCopy != null) ...[
                      SizedBox(height: 6),
                      Text(
                        item.catchCopy!,
                        style: GoogleFonts.notoSerifJp(
                          fontSize: 12,
                          fontStyle: FontStyle.italic,
                          color: Color(0xFF376495),
                        ),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

レイアウトイメージ:

┌────────────────────────────────────┐
│ [120x120] │ 獺祭               ← 明朝体18px
│   写真    │ 旭酒造 | 山口県    ← ゴシック体11px
│  角丸8px  │ ⭐⭐⭐⭐⭐ 純米大吟醸
│           │ "夜風と楽しみたい、淡麗な一滴"
└────────────────────────────────────┘
  ↑ 0.5pxのボーダーで区切り

🍶 Gemini 3.0統合(核心機能)

APIキー設定

// lib/secrets.dart
class Secrets {
  static const String geminiApiKey = 'AIzaSyA2BSr16R2k0bHjSYcSUdmLoY8PKwaFts0';
}

リアルタイム実況付きGemini解析

// lib/services/gemini_service.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'dart:convert';
import '../secrets.dart';

class GeminiService {
  late final GenerativeModel _model;

  GeminiService() {
    _model = GenerativeModel(
      model: 'gemini-2.5-flash-latest',  // スキャンアプリで実績あり
      apiKey: Secrets.geminiApiKey,
      generationConfig: GenerationConfig(
        temperature: 0.1,
      ),
    );
  }

  /// リアルタイム実況付きで解析
  Stream<String> analyzeSakeLabelWithCommentary(Uint8List imageBytes) async* {
    // ステージ1: 開始
    yield 'ラベルを読んでいます...';
    await Future.delayed(Duration(milliseconds: 500));

    // ステージ2: 解析中
    yield 'お酒の情報を確認しています...';

    try {
      final prompt = '''
この画像は日本酒のボトルまたはラベルの写真です。
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。

【重要な指示】
1. ラベルに明確に書かれている情報のみを抽出してください
2. 推測や想像で値を入れないでください読めない場合はnullまたは省略
3. 数値は必ず数字のみ(単位記号%などは除く)
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください

【出力フォーマット】
{
  "brandName": "銘柄名",
  "type": "特定名称",
  "alcoholContent": 数値,
  "polishingRatio": 数値,
  "breweryName": "酒蔵名",
  "prefecture": "都道府県名",
  "catchCopy": "夜風と楽しみたい、淡麗な一滴"
}

**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
''';

      final content = [
        Content.multi([
          TextPart(prompt),
          DataPart('image/jpeg', imageBytes),
        ])
      ];

      final response = await _model.generateContent(content);
      final text = response.text ?? '';

      // ステージ3: 都道府県を検出した場合
      if (text.contains('県')) {
        final prefectureMatch = RegExp(r'([^\s]+県)').firstMatch(text);
        if (prefectureMatch != null) {
          yield 'お、これは${prefectureMatch.group(1)}の銘柄ですね...';
          await Future.delayed(Duration(milliseconds: 300));
        }
      }

      // ステージ4: データ整理中
      yield 'データを整理しています...';
      await Future.delayed(Duration(milliseconds: 300));

      // JSON抽出
      final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
      if (jsonMatch != null) {
        final jsonString = jsonMatch.group(0)!;
        final data = jsonDecode(jsonString);

        // ステージ5: 完了
        yield '解析完了!';

        // データを返す(特別な形式)
        yield 'DATA:$jsonString';
      } else {
        yield 'エラー: データを読み取れませんでした';
      }
    } catch (e) {
      yield 'エラー: $e';
    }
  }

  /// シンプルな解析(実況なし)
  Future<Map<String, dynamic>?> analyzeSakeLabel(Uint8List imageBytes) async {
    try {
      final prompt = '''
この画像は日本酒のボトルまたはラベルの写真です。
ラベルに書かれているテキスト情報を正確に読み取り、JSON形式で返してください。

【重要な指示】
1. ラベルに明確に書かれている情報のみを抽出してください
2. 推測や想像で値を入れないでください読めない場合はnullまたは省略
3. 数値は必ず数字のみ(単位記号%などは除く)
4. このお酒の印象を一言で表す「キャッチコピー」を自動生成してください

【出力フォーマット】
{
  "brandName": "銘柄名",
  "type": "特定名称",
  "alcoholContent": 数値,
  "polishingRatio": 数値,
  "breweryName": "酒蔵名",
  "prefecture": "都道府県名",
  "catchCopy": "夜風と楽しみたい、淡麗な一滴"
}

**JSON以外の余計な説明は一切不要です。JSONのみを出力してください。**
''';

      final content = [
        Content.multi([
          TextPart(prompt),
          DataPart('image/jpeg', imageBytes),
        ])
      ];

      final response = await _model.generateContent(content);
      final text = response.text ?? '';

      final jsonMatch = RegExp(r'\{[\s\S]*\}').firstMatch(text);
      if (jsonMatch != null) {
        final jsonString = jsonMatch.group(0)!;
        return jsonDecode(jsonString);
      }

      return null;
    } catch (e) {
      debugPrint('Gemini API Error: $e');
      return null;
    }
  }
}

リアルタイム実況の使用例

// lib/screens/input_screen.dart一部
StreamBuilder<String>(
  stream: geminiService.analyzeSakeLabelWithCommentary(imageBytes),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      final message = snapshot.data!;

      // データが返ってきた場合
      if (message.startsWith('DATA:')) {
        final jsonString = message.substring(5);
        final data = jsonDecode(jsonString);
        // フォームに自動入力
        _fillForm(data);
        return ResultForm(data: data);
      }

      // 実況メッセージを表示
      return AnalyzingOverlay(currentStage: message);
    }

    return AnalyzingOverlay(currentStage: 'カメラを起動しています...');
  },
)

📦 データモデル

SakeItem (Hive Model)

// lib/models/sake_item.dart
import 'package:hive/hive.dart';

part 'sake_item.g.dart';

@HiveType(typeId: 0)
class SakeItem extends HiveObject {
  @HiveField(0)
  String? brandName;

  @HiveField(1)
  String? breweryName;

  @HiveField(2)
  String? prefecture;

  @HiveField(3)
  String? type;

  @HiveField(4)
  double? alcoholContent;

  @HiveField(5)
  int? polishingRatio;

  @HiveField(6)
  String imagePath;

  @HiveField(7)
  List<String>? additionalImages;

  @HiveField(8)
  double? rating;

  @HiveField(9)
  String? memo;

  @HiveField(10)
  List<String>? tags;

  @HiveField(11)
  bool isFavorite;

  @HiveField(12)
  bool isWishlist;

  @HiveField(13)
  DateTime createdAt;

  @HiveField(14)
  DateTime? updatedAt;

  @HiveField(15)
  int? price;

  @HiveField(16)
  int? volume;

  @HiveField(17)
  String? catchCopy;  // AIが生成したキャッチコピー

  @HiveField(18)
  double? sweetnessScore;  // -1.0(辛口)~ 1.0(甘口)

  @HiveField(19)
  double? bodyScore;  // -1.0(淡麗)~ 1.0(濃醇)

  SakeItem({
    this.brandName,
    this.breweryName,
    this.prefecture,
    this.type,
    this.alcoholContent,
    this.polishingRatio,
    required this.imagePath,
    this.additionalImages,
    this.rating,
    this.memo,
    this.tags,
    this.isFavorite = false,
    this.isWishlist = false,
    required this.createdAt,
    this.updatedAt,
    this.price,
    this.volume,
    this.catchCopy,
    this.sweetnessScore,
    this.bodyScore,
  });
}
# 生成コマンド
flutter pub run build_runner build

🚀 実装チェックリスト

Phase 1: MVP5時間

  • プロジェクト作成(flutter create ponshu_room_reborn
  • pubspec.yaml 設定
  • Android設定compileSdk: 36, targetSdk: 35
  • lib/secrets.dart 作成APIキー
  • lib/models/sake_item.dart 作成
  • flutter pub run build_runner build
  • lib/theme/app_theme.dart 作成posimaiカラー
  • lib/main.dart 作成Hive初期化
  • lib/screens/home/home_screen.dart 作成4タブ
  • lib/services/gemini_service.dart 作成(リアルタイム実況)
  • lib/widgets/sake_card.dart 作成(フル幅カード)
  • lib/widgets/analyzing_overlay.dart 作成実況UI
  • カメラ撮影機能
  • 入力フォーム
  • 詳細画面
  • SafeArea対応 ← Android 15必須

Phase 2: 「美録」UI洗練3時間

  • 余白調整padding: 8dp, separator: 4dp
  • 明朝体×ゴシック体の適用
  • Hero遷移
  • Instagram用画像生成機能

Phase 3: 「遊び心」機能拡張4時間

  • フレーバー・マトリックス
  • 日本酒・制覇マップ + バッジ
  • AIソムリエ質問例、チャット
  • マイページ統計グラフ

Phase 4: 共有機能2時間

  • シンプルテキスト共有
  • Instagram用正方形画像生成
  • キャッチコピー付き共有

完成基準

MVP完成の定義

  • カメラで日本酒を撮影できる
  • Gemini 3.0でリアルタイム実況しながら解析
  • キャッチコピーが自動生成される
  • データをHiveに保存できる
  • フル幅カード写真120x120で表示
  • 詳細表示できる
  • SafeAreaで見切れない
  • Android 15 (Xiaomi 14T Pro) で動作する

最終完成の定義

  • すべての機能が動作
  • フレーバー・マトリックス表示
  • 日本酒・制覇マップ + バッジ
  • Instagram用正方形画像生成
  • 「雑誌のような」デザイン
  • 60fpsの滑らかな動作
  • 「魔法のような」心地よさ

📞 進捗報告

各フェーズ完了後に以下を報告してください:

Phase 1-1 完了: プロジェクト初期化
Phase 1-2 完了: データモデル
...

問題点:
- XXXでエラーが発生しました
- YYYの実装方法が不明です

質問:
- ZZZはどう実装すべきですか

頑張ってください!「魔法のような体験」を一緒に作りましょう!🍶