[Figma/Design] Figma でのタイポグラフィ基礎

🎯 UIデザインに向いているフォントの特徴
クセが少ない(可読性が高い)
太字・中太・通常など ウェイトの種類が多い
大きさや太さを変えても破綻しない

👍 よく使われる無料フォント
● Noto Sans / Noto Sans JP(Google)
→ 一番無難。日本語も英語も揃う。

● Inter
→ グローバルで最も使われるUIフォントの1つ。
→ Figma公式フォントにも入っている。

● Roboto / Roboto Condensed
→ Android系UIで多用される。

● SF Pro(Mac)
→ iOS / macOSで使用。

2. 階層(Typographic Hierarchy)
タイポの階層とは、ユーザーが画面を見た時に
どこが重要で、どこが補足なのかが一目で分かるようにする仕組み です。

典型的な階層構造(UI用)
役割 例 サイズ(px) 重さ(Weight)
H1(大見出し) 画面タイトル 24–32 Bold / SemiBold
H2(中見出し) セクションタイトル 20–24 Medium / SemiBold
Body(本文) 説明文 14–16 Regular / Medium
Caption(補足) ラベル/注釈 12–13 Regular

3. 行間(Line Height)
行間は文章の可読性に直結する

UIデザインでの基本値は:

● 本文(14–16px)

➡ Line height:120%〜160%(1.2〜1.6)

● 見出し(20px以上)

➡ Line height:110%〜130%

Figmaでの設定方法

テキストを選択 → 右の Line height に数値入力
例:

16px フォント → Line height 24px(=150%)

4. 文字間(Letter spacing)
文字間は、視認性や雰囲気を整えるために使います。
UIの一般指標
本文(14–16px):0〜1%
字幕・小さい文字(12px前後):1~3%
大きい見出し:0%以下でも良い(−1% など)

Figmaでは
Text → Letter spacing
で数値を入力(% か px)できます。

5. 日本語タイポの注意点

✓ 日本語は欧文より“詰まりやすい”ため
→ 行間は少し広め(150%前後)がおすすめ。

✓ フォントの種類によっては縦方向の見え方が違う
→ 見出しは Noto Sans JP Bold
→ 本文は Noto Sans JP Regular
など役割でフォントウェイトを分ける。

✓ UIでは明朝体が読みにくいので避ける(特別な用途を除く)

なるほど、全然意識していなかったけど、見やすくするのですね。納得です。

[Android/iOS] ストア申請の流れ

以下に Android(Google Play)と iOS(App Store) のストア申請の流れをわかりやすくまとめ ました。
「開発者登録 → 審査 → 公開」まで、現場で実際に行う手順ベースです。

📱 Android(Google Play)ストア申請の流れ
✅ 1. Google Play デベロッパー登録
Google アカウントで登録
登録料:25ドル(買い切り)

✅ 2. Google Play Console にアプリを作成
アプリ名入力
パッケージ名(com.example.app)が必要
アプリ種別(アプリ / ゲーム)
無料 / 有料の選択

✅ 3. アプリの情報入力
以下を必ず入力する必要があります:
ストア掲載情報
アプリ名
短い説明
長い説明
アイコン
スクリーンショット
プロモーション画像
Appカテゴリ(ジャンル・コンテンツレーティング)
プライバシーポリシーURL
アプリのアクセス権に関する申請(位置情報・カメラ等)

✅ 4. App Bundle(AAB)をアップロード
Android Studio → Build > Generate Signed App Bundle
keystore で署名
AAB を Play Console にアップロード

✅ 5. 審査リクエスト(リリース)
プロダクションリリースへ申請
審査期間:数時間~3日程度
新人アカウントほど長くなる傾向あり

✅ 6. 公開
公開後、数時間でストアに反映される
ホットフィックスは数時間で更新可能

🍎 iOS(App Store)ストア申請の流れ
✅ 1. Apple Developer Program 登録
料金:年間 99ドル
個人 / 企業で登録形態が異なる
iPhone実機テストには Developer Program が必要

✅ 2. App Store Connect でアプリ作成
アプリ名
バンドルID(Xcode側設定と一致)
SKU の設定
カテゴリ選択

✅ 3. アプリの情報入力(詳細)
App Store用の説明情報
タイトル
サブタイトル
プロモーションテキスト
スクリーンショット(6.7インチ / 5.5インチ 必須)
アイコン(1024×1024)
年齢レーティング
プライバシーポリシーURL
データ収集とトラッキングの申告(AppTrackingTransparency)

✅ 4. Xcode でビルドしてアップロード
Xcode → Archive → Distribute App
Transporter(Apple純正)経由でもOK
Upload 後、App Store Connect の「TestFlight」に表示される

✅ 5. App Review に提出
審査に必要な入力:
デモアカウント(ログイン必要アプリの場合)
審査用メモ
有料アイテムの申請(In-App Purchaseがある場合)
審査期間:1~3日程度(早ければ半日)

✅ 6. リリース(公開)
審査OK → 公開日を即時または手動で選択
公開後、数時間で App Store へ反映

🔍 Android と iOS の比較(重要ポイント)
項目 Android iOS
開発者登録 25ドル(買い切り) 99ドル/年
審査 数時間〜3日 半日〜3日
ビルド形式 AAB IPA(Xcodeからアップロード)
審査の厳しさ やや緩い かなり厳格
更新反映 早い やや遅い
🎯 まとめ(全

[android] figmaのデザインのandroidへの落とし方

1. figmaから以下の情報を取得する
🎨 色(Color)
#RRGGBB または RGBA
不透明度(Opacity)
グラデーション

🔤 文字(Typography)
フォント名
サイズ(sp)
行間(lineHeight)
Font Weight

🔲 余白(Spacing)
padding / margin(dp)
コンポーネントの width/height

📐 角丸(Corner Radius)
dp

⭐ シャドウ(Elevation / shadow)
shadow の offset / blur / color

2.ザイントークンとして Android へ落とし込む
Figma の色・文字・spacing 情報をそのままコードに書くのではなく、
“テーマ” として一箇所にまとめる のがプロのやり方

Jetpack Compose のテーマ例(Theme.kt)

object AppColors {
    val Primary = Color(0xFF4CAF50)
    val Secondary = Color(0xFF03A9F4)
    val TextPrimary = Color(0xFF333333)
}

object AppTypography {
    val Title = TextStyle(
        fontSize = 20.sp,
        fontWeight = FontWeight.Bold
    )
    val Body = TextStyle(
        fontSize = 14.sp
    )
}

object AppRadius {
    val Medium = 12.dp
}

object AppSpacing {
    val Small = 8.dp
    val Medium = 16.dp
}

3.Compose の UI 実装(Figma → Compose への変換)
Figma のボタンを例にします。

🎨 Figma のボタン例
幅:200
高さ:48
角丸:12
背景色:#4CAF50

テキスト:白、16sp、Medium

@Composable
fun AppButton(text: String, onClick: () -> Unit) {
    Box(
        modifier = Modifier
            .size(width = 200.dp, height = 48.dp)
            .background(AppColors.Primary, shape = RoundedCornerShape(AppRadius.Medium))
            .clickable { onClick() },
        contentAlignment = Alignment.Center
    ) {
        Text(text, style = TextStyle(color = Color.White, fontSize = 16.sp))
    }
}

これをcomposeとして使う

<Button
    android:layout_width="200dp"
    android:layout_height="48dp"
    android:background="@drawable/rounded_button"
    android:text="Button"
    android:textSize="16sp"
    android:textColor="@android:color/white" />

[android] Dependency Injection(Hilt)

Hilt を使って依存性(Repository)を注入する最小例
Activity から Repository を直接 new しない → テストしやすい設計

MyRepositoy.kt

interface MyRepository {
    fun getMessage(): String
}

class MyRepositoryImpl : MyRepository {
    override fun getMessage() = "Hello from Repository!"
}

AppModule.kt

package com.example.hiltsample

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideMyRepository(): MyRepository = MyRepositoryImpl()
}

MainActivity.kt

// MainActivity.kt
package com.example.hiltsample

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    @Inject lateinit var repository: MyRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // DI された repository が使える
            androidx.compose.material3.Text(text = repository.getMessage())
        }
    }
}

Activity が Repository に依存しない(疎結合)
FakeRepository に差し替えるだけで テストが簡単
Repository の作り方を 1 箇所にまとめられて 保守性が上がる

Lipsyncの環境を作る

ripsyncに必要な rhubarb というファイルをgithubからダウンロードする
OSに合わせる必要があるため、rhubarb の mac osをダウンロードする
https://github.com/DanielSWolf/rhubarb-lip-sync/releases

$ mkdir vrm-project

この vrm-project に rhubarb を配置する

$ chmod +x rhubarb

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Simple VRM Lipsync (Final)</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
        #playButton { 
            position: absolute; 
            top: 20px; 
            left: 20px; 
            padding: 10px 20px;
            font-size: 16px;
            z-index: 10;
        }
    </style>
</head>
<body>
    <button id="playButton" disabled>VRMとデータをロード中...</button>

    <script src="https://unpkg.com/three@0.158.0/build/three.min.js"></script>

    <script src="https://unpkg.com/three@0.158.0/examples/js/controls/OrbitControls.js"></script>

    <script src="https://unpkg.com/three@0.158.0/examples/js/loaders/GLTFLoader.js"></script>
    
    <script>
        // 1. OrbitControls の定義をコピー
        if (typeof window.OrbitControls !== 'undefined' && typeof THREE.OrbitControls === 'undefined') {
            THREE.OrbitControls = window.OrbitControls;
        }
        
        // 2. GLTFLoader の定義をコピー
        if (typeof window.GLTFLoader !== 'undefined' && typeof THREE.GLTFLoader === 'undefined') {
            THREE.GLTFLoader = window.GLTFLoader;
        }
    </script>
    
    <script src="https://unpkg.com/@pixiv/three-vrm@2.0.0/lib/three-vrm.min.js"></script>
    
    <script>
        // --- 設定値 ---
        const VRM_MODEL_PATH = './28538335112454854.vrm'; 
        const AUDIO_PATH = './output.wav'; 
        const LIPSYNC_JSON_PATH = './output_lipsync.json';
        
        let renderer, scene, camera, clock, vrm, audio, lipsyncData;
        
        let vrmLoaded = false;
        let jsonLoaded = false;
        let currentVisemeIndex = 0;

        function checkReady() {
            if (vrmLoaded && jsonLoaded) {
                 const button = document.getElementById('playButton');
                 button.innerHTML = '音声再生&リップシンク開始';
                 button.disabled = false;
            }
        }
        
        // --- 初期化処理 ---
        function init() {
            // 1. レンダラーのセットアップ
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setClearColor(0xeeeeee);
            document.body.appendChild(renderer.domElement);

            // 2. シーンとカメラのセットアップ
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
            camera.position.set(0, 1.3, 1.5); 
            
            // 3. ライト
            scene.add(new THREE.AmbientLight(0xffffff, 1.0));

            // 4. コントロール (OrbitControlsが見つからない場合はスキップ)
            let OrbitControlsClass = THREE.OrbitControls;
            
            if (!OrbitControlsClass) {
                console.error("致命的エラー: OrbitControlsが見つかりません。カメラ操作はできませんが、続行します。");
            } else {
                const controls = new OrbitControlsClass(camera, renderer.domElement);
                controls.target.set(0, 1.3, 0); 
            }

            // 5. アニメーションクロック
            clock = new THREE.Clock();
            
            // 6. VRMとJSONデータのロード
            loadVRM();
            loadLipsyncData();
            
            // 7. リップシンク開始ボタン
            document.getElementById('playButton').addEventListener('click', startLipsync);

            // 8. ウィンドウリサイズ対応
            window.addEventListener('resize', onWindowResize);
            
            // 9. アニメーションループ開始
            animate();
        }

        // --- VRMモデルのロード ---
        function loadVRM() {
            const loader = new THREE.GLTFLoader(); // ★修正されたGLTFLoaderを参照
            loader.crossOrigin = 'anonymous';

            loader.load(
                VRM_MODEL_PATH,
                (gltf) => {
                    THREE.VRM.from(gltf).then((vrmInstance) => {
                        vrm = vrmInstance;
                        scene.add(vrm.scene);
                        
                        VRMUtils.rotateVRM0(vrm); 
                        vrm.scene.rotation.y = Math.PI;
                        
                        resetMouth(); 
                        
                        vrmLoaded = true;
                        checkReady();
                    })
                    .catch(e => {
                         console.error("VRMインスタンス生成中にエラーが発生しました(互換性問題の可能性):", e);
                    });
                },
                undefined,
                (error) => {
                    console.error("GLTFファイルのロード中にエラーが発生しました。", error);
                }
            );
        }

        // --- リップシンクデータのロード (省略) ---
        function loadLipsyncData() {
            fetch(LIPSYNC_JSON_PATH)
                .then(response => {
                    if (!response.ok) {
                        throw new Error(`HTTP error! status: ${response.status}`);
                    }
                    return response.json();
                })
                .then(data => {
                    lipsyncData = data;
                    jsonLoaded = true;
                    checkReady();
                })
                .catch(error => {
                    console.error('Error loading lipsync JSON:', error);
                });
        }

        // --- 口のブレンドシェイプをリセットする関数 (省略) ---
        function resetMouth() {
            if (!vrm) return;
            const blendShapeProxy = vrm.humanoid.getBlendShapeProxy();
            blendShapeProxy.setValue('vrc.v_a', 0.0);
            blendShapeProxy.setValue('vrc.v_i', 0.0);
            blendShapeProxy.setValue('vrc.v_u', 0.0);
            blendShapeProxy.setValue('vrc.v_e', 0.0);
            blendShapeProxy.setValue('vrc.v_o', 0.0);
        }

        // --- リップシンク開始 (省略) ---
        function startLipsync() {
            if (!vrm || !lipsyncData || document.getElementById('playButton').disabled) {
                return;
            }
            
            document.getElementById('playButton').disabled = true;
            document.getElementById('playButton').innerHTML = '再生中...';

            audio = new Audio(AUDIO_PATH);
            audio.play();
            
            audio.onended = () => {
                resetMouth();
                currentVisemeIndex = 0;
                document.getElementById('playButton').innerHTML = '音声再生&リップシンク開始';
                document.getElementById('playButton').disabled = false;
            };
        }
        
        // --- リップシンク実行関数 (省略) ---
        function performLipsync() {
            if (!audio || audio.paused || audio.ended || !vrm) return;

            const currentTime = audio.currentTime;
            const visemes = lipsyncData.mouthCues;
            const blendShapeProxy = vrm.humanoid.getBlendShapeProxy();
            
            while (currentVisemeIndex < visemes.length) {
                const currentCue = visemes[currentVisemeIndex];
                
                if (currentTime < currentCue.start) {
                    break; 
                }
                
                if (currentTime >= currentCue.end) {
                    currentVisemeIndex++;
                    continue;
                }

                resetMouth(); 

                const rhubardValue = currentCue.value;

                // RhubarbのVisemeとVRMキーのマッピング
                const visemeKey = {
                    'A': 'vrc.v_a', 'B': 'vrc.v_o', 'C': 'vrc.v_i', 
                    'D': 'vrc.v_e', 'E': 'vrc.v_e', 'F': 'vrc.v_o', 
                    'G': 'vrc.v_e', 'H': 'vrc.v_a', 'X': null
                }[rhubardValue];

                if (visemeKey) {
                    blendShapeProxy.setValue(visemeKey, 1.0);
                }
                
                break; 
            }
        }

        // --- アニメーションループ (省略) ---
        function animate() {
            requestAnimationFrame(animate);

            const deltaTime = clock.getDelta();
            
            if (vrm) {
                vrm.update(deltaTime);
                performLipsync();
            }

            renderer.render(scene, camera);
        }

        // --- リサイズ処理 (省略) ---
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        // アプリケーション開始
        init();
    </script>
</body>
</html>

[動画] Pexels Videos

Pexels Videos(ペクセル・ビデオ)は、著作権フリーの高品質な動画素材を無料でダウンロードできるウェブサイトです。

写真素材で有名な「Pexels」の動画版として公開されており、主な特徴は以下の通りです。

🎬 Pexels Videosの主な特徴
完全無料: 会員登録をしなくても、動画素材を無料でダウンロードできます。
高品質・高解像度: プロやアマチュアのクリエイターがアップロードした、質の高い動画素材が豊富に揃っています。
商用利用可能: ダウンロードした素材は、営利目的(ビジネス用途)でも安心して利用できます。
著作権フリー(ロイヤリティフリー):
クレジット表記(出典表示)が不要です。

素材の加工や編集も自由に行えます(一部を切り取る、色味を変えるなど)。
日本語検索対応: 海外のサイトですが、日本語でのキーワード検索に対応しており、素材を探しやすいです。
動画制作やプレゼンテーション、ソーシャルメディアへの投稿など、様々な用途でプロフェッショナルな映像を手軽に使用したい場合に非常に便利なサービスです。

なるほど、ここでDLして編集などに使えますね^^ OK

iOS トランジション

🔹 トランジション(Transition)とは

簡単に言うと:
画面A → 画面B へ移動する時の “見せ方” のことです。

例えば:
フェードイン(ゆっくり現れる)
スライド(横にスッと移動)
スケール(拡大・縮小しながら表示)

🔹 SwiftUIでのトランジション例
SwiftUI では .transition() モディファイアで簡単に書けます。

✅ 基本例:フェードイン・フェードアウト

import SwiftUI

struct ContentView: View {
    @State private var showBox = false

    var body: some View {
        VStack(spacing: 20) {
            Button("切り替え") {
                withAnimation {
                    showBox.toggle()
                }
            }

            if showBox {
                Rectangle()
                    .fill(Color.blue)
                    .frame(width: 200, height: 200)
                    .transition(.opacity) // ← フェードイン/アウト
            }
        }
    }
}

🌀 withAnimation { … }
➡️ アニメーション付きで状態を切り替える、という意味です。

✅ 別のトランジション例
トランジション名 動き
.slide 横からスライドイン/アウト
.scale 拡大・縮小
.move(edge: .bottom) 下から出てくる
.opacity フェードイン/アウト
.asymmetric 表示と非表示で違う動き

例:

.transition(.move(edge: .bottom))

✅ 複合トランジション

2つ以上組み合わせも可能です:

.transition(.move(edge: .bottom).combined(with: .opacity))

これで「下からスライドしながらフェードイン」が実現できます。

🔹 Navigationトランジション

もう一つ、「NavigationView」などでページ遷移するときにも自動的にトランジションが入ります。

例:

NavigationLink(destination: NextView()) {
    Text("次へ")
}

➡️ デフォルトで 右から左へスライドイン
(戻るときは左から右へ)

UIKit時代だと UIViewController の present(_:animated:) などを使ってトランジションを細かく制御しましたが、
SwiftUI では シンプルな構文 で済むようになっています。

🔹 まとめ
用語 意味
トランジション(Transition) 画面や要素が出入りする動き(アニメーション)
アニメーション(Animation) 値や状態の変化に伴う動き
withAnimation 状態変更をアニメーションで包む構文

figma アクセスビリティ

Figmaにおける アクセシビリティ(Accessibility) とは、
「誰でも使いやすいデザインをつくるための配慮を、Figma上で設計段階から行うこと」
を指します。

🎯 アクセシビリティとは
アクセシビリティ(Accessibility)は、
障害のある人・高齢者・色覚特性のある人などを含めた すべてのユーザーが使いやすいデザイン にすることを目的としています。
Figmaでは主に ビジュアルと操作の面 でそれを支援します。

🧭 Figmaで配慮すべき主なアクセシビリティ項目
1. 色のコントラスト
テキストと背景の明度差を確保します(WCAG 2.1 準拠)。
通常の文字:コントラスト比 4.5:1以上
大きな文字(18pt以上または太字14pt以上):3:1以上

🔧 Figmaプラグインで確認可能:
“Contrast”
“Able”
“Stark”(有名なアクセシビリティチェックプラグイン)

2. 色だけに頼らない情報設計
色の変化(例:赤=エラー)だけで意味を伝えない。
例)アイコン(⚠️)やテキスト(“Error”)も併用。
状態(有効・無効・警告など)を 形やラベル でも伝える。

3. フォントサイズ・行間
小さすぎる文字は避ける(最低 12〜14pt 推奨)。
行間(line height)は1.4〜1.6倍程度を目安にする。
Auto Layout を使うと、テキストが拡大されても崩れにくくなります。

4. キーボード操作・フォーカスの可視化(UI設計段階で)
クリックだけでなく、Tabキー操作 で要素を辿れる設計を意識。
フォーカス時に**明確なハイライト(アウトラインや影)**をデザインしておく。

5. 音声リーダー(スクリーンリーダー)対応を意識
Figma上では直接読み上げ設定はできませんが、
ボタンやリンクに 明確なテキストラベル を付ける
意味のあるコンポーネント名 を使う(例:「btn_submit」ではなく「Button / Primary」など)
開発者が読み上げラベルを設定しやすくなります。

6. レスポンシブ対応・レイアウトの柔軟性
Auto Layout と Constraints を活用し、
画面サイズが変わっても情報が欠けないようにする。
テキスト量が増えてもボタンが崩れない設計に。

7. テキストコントラスト・シミュレーション
🔧 プラグイン “Sim Daltonism” や “Contrast” を使うと、
色覚特性(赤緑・青黄など)での見え方を確認可能。

🧩 Figmaでの実践例
ボタンコンポーネント に対して:
ホバー・フォーカス・無効状態をそれぞれVariantで定義
コントラスト比を4.5:1以上に調整
テキストラベルを付け、アイコンだけのボタンは避ける
フォーム では:
入力欄のエラー時に、赤色+メッセージ+アイコンで通知
必須項目には「*」とテキスト補足を併用

💡 まとめ
項目 目的 Figmaでの対応方法
コントラスト 見やすくする Contrastプラグインなどで確認
色依存を避ける 意味を正確に伝える 形・テキストも併用
テキストサイズ 読みやすく 最低12〜14pt、line-height 1.5倍
フォーカス可視化 操作性を確保 Hover/Focus variantで表現
名前付け スクリーンリーダー補助 意味のあるレイヤー・コンポ名に

llm メール返信bot

📨 受信メール

🧠 LLM(GPT / Gemini / Claude)
├── メールの要約
├── 返信意図の理解
└── 自動返信文の生成

✉️ 返信メール送信(Gmail API / Outlook API)

import os
import base64
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from openai import OpenAI

# --- LLM設定 ---
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# --- Gmail設定 ---
creds = Credentials.from_authorized_user_file("token.json", ["https://www.googleapis.com/auth/gmail.modify"])
service = build("gmail", "v1", credentials=creds)

# 直近メール取得
results = service.users().messages().list(userId="me", maxResults=1, q="in:inbox").execute()
message = service.users().messages().get(userId="me", id=results["messages"][0]["id"], format="full").execute()
email_data = base64.urlsafe_b64decode(message["payload"]["parts"][0]["body"]["data"]).decode()

# LLMに返信文生成を依頼
prompt = f"""
以下のメールに対して、丁寧で簡潔な返信を日本語で作成してください。

--- メール本文 ---
{email_data}
"""
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": prompt}]
)

reply_text = response.choices[0].message.content
print("=== 返信案 ===")
print(reply_text)

GoogleのText-to-Speech APIを使ってみる

1. Google Cloud プロジェクト作成
2. Text-to-Speech API を有効化
3. サービスアカウントキー(JSON)をダウンロード
4. 必要なライブラリをインストール
$ pip3 install google-cloud-texttospeech

from google.cloud import texttospeech
from google.oauth2 import service_account

# 認証キー読み込み
credentials = service_account.Credentials.from_service_account_file("gcp-test.json")
client = texttospeech.TextToSpeechClient(credentials=credentials)

# 読み上げるテキスト
input_text = texttospeech.SynthesisInput(text="こんにちは、これはテストです。")

# 日本語のWavenet音声
voice = texttospeech.VoiceSelectionParams(
    language_code="ja-JP",
    name="ja-JP-Wavenet-B"
)

audio_config = texttospeech.AudioConfig(
    audio_encoding=texttospeech.AudioEncoding.MP3
)

# 音声生成
response = client.synthesize_speech(
    input=input_text,
    voice=voice,
    audio_config=audio_config
)

# 保存
with open("output.mp3", "wb") as out:
    out.write(response.audio_content)
    print("✅ output.mp3 を作成しました")

そりゃGoogleもやってるわな… という感想しか出てこない