YouTube動画のタイトルと概要欄を自動で多言語化するツールを作ってみた

YouTubeに動画を投稿する際、日本語だけで終わらせていませんか? 今回は、Pythonを使ってYouTube動画のタイトルと概要欄を一括で多言語化(ローカライズ)するツールを作成したので、その振り返りを記録します。 多言語化ツールを作った背景 YouTubeには「タイトルの翻訳機能」がありますが、一つひとつ手動で入力するのは非常に手間がかかります。 そこで、Python を使って、日本語のタイトルと概要欄を元に、世界各国の言語へ自動翻訳・登録するスクリプトを作成しました。 Vibe Codingでの爆速開発 今回の開発スタイルはいわゆるバイブコーディングです。 インフラ系なので、開発のカの字もないんですよね(笑) import os import json import logging from typing import Dict, List, Tuple from dotenv import load_dotenv from openai import OpenAI from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.errors import HttpError # ========================= # 設定 # ========================= BASE_LANG = "ja" # 元言語は日本語 # 対応言語(YouTube言語コード → 名前とオーディエンス説明) LANG_CONFIG: Dict[str, Dict[str, str]] = { "en": {"name": "US English", "audience": "viewers in North America and other English-speaking countries"}, "es": {"name": "European Spanish", "audience": "viewers in Spain"}, "es-419": {"name": "Latin American Spanish", "audience": "viewers in Mexico and other Latin American countries"}, "fr": {"name": "French", "audience": "viewers in France and other French-speaking regions"}, "de": {"name": "German", "audience": "viewers in Germany, Austria, and Switzerland"}, "pt-BR": {"name": "Brazilian Portuguese", "audience": "viewers in Brazil"}, "ru": {"name": "Russian", "audience": "viewers in Russia and neighboring countries"}, "zh-Hans": {"name": "Simplified Chinese", "audience": "viewers in Mainland China"}, "zh-Hant": {"name": "Traditional Chinese", "audience": "viewers in Taiwan, Hong Kong, and other Traditional Chinese regions"}, "ar": {"name": "Arabic", "audience": "viewers in Arabic-speaking countries"}, "id": {"name": "Indonesian", "audience": "viewers in Indonesia"}, "ko": {"name": "Korean", "audience": "viewers in South Korea"}, } # YouTube Data API スコープ SCOPES = ["https://www.googleapis.com/auth/youtube.force-ssl"] # ========================= # ロガー設定 # ========================= logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger(__name__) # Load environment variables from .env if present load_dotenv() # ========================= # クライアント生成 # ========================= def get_youtube_client(): """YouTube Data API クライアントを返す。""" logger.info("YouTube API の認証を開始します...") flow = InstalledAppFlow.from_client_secrets_file( "client_secret.json", SCOPES ) try: creds = flow.run_local_server(port=0, timeout=120) except KeyboardInterrupt: logger.info("認証をユーザーが中断しました。終了します。") raise SystemExit(1) youtube = build("youtube", "v3", credentials=creds) logger.info("YouTube API クライアントを取得しました。") return youtube def get_openai_client() -> OpenAI: """OpenAI クライアントを返す。""" api_key = os.environ.get("OPENAI_API_KEY") if not api_key: logger.error("環境変数 OPENAI_API_KEY が設定されていません。") raise RuntimeError("OPENAI_API_KEY is not set.") logger.info("OpenAI クライアントを初期化しました。") return OpenAI(api_key=api_key) # ========================= # OpenAI によるローカライズ # ========================= def localize_for_language( client: OpenAI, base_title: str, base_desc: str, yt_lang: str, lang_meta: Dict[str, str], ) -> Tuple[str, str]: """ 1言語分のタイトル・概要を OpenAI でローカライズする。 戻り値: (localized_title, localized_description) """ language_name = lang_meta["name"] audience = lang_meta["audience"] system_prompt = ( "You are an expert YouTube copywriter and localization specialist.\n" "You receive the original Japanese title and description for a YouTube video.\n" "Your job is NOT to translate literally, but to localize them so they sound natural, persuasive, and engaging " "for the target language and culture.\n" "Respect the original intent, topic, and tone (for example: educational, entertaining, serious, casual, etc.), " "while optimizing for YouTube viewers in the specified audience region.\n" "Avoid clickbait and misleading exaggeration, but make the title and description attractive and clear.\n" "You must also suggest appropriate hashtags in the target language (and English if commonly used) that help reach the right audience.\n" "Return a JSON object with exactly three fields: 'title', 'description', and 'hashtags'.\n" "'hashtags' must be a JSON array of strings, each string being a hashtag including the leading '#'.\n" "No extra commentary, no markdown, only the JSON object." ) user_prompt = f""" [Target language] - Language: {language_name} - YouTube language code: {yt_lang} - Audience: {audience} [Original Japanese title] {base_title} [Original Japanese description] {base_desc} [Requirements] - Keep the core meaning and key information of the original Japanese text. - Adapt expressions and nuance so they feel natural and appealing in the target language and culture. - Make the title concise and catchy for YouTube (ideally under ~70 characters in this language, if reasonable). - In the description, use 2–4 short paragraphs for readability. - Maintain a tone similar to the original (for example: friendly, curious, educational, relaxed, serious, etc.). - Do NOT put hashtags inside the description. - Instead, provide 5–12 appropriate hashtags in the 'hashtags' field, as a JSON array of strings. - Include topic-specific hashtags (related to the content of the video). - Optionally include a few general YouTube or genre-related hashtags (e.g., about the format or category). - Use only the target language (and English where it is naturally used for hashtags). """ try: resp = client.chat.completions.create( model="gpt-4.1-mini", # 必要に応じてモデルは変更可 response_format={"type": "json_object"}, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.8, ) except Exception as e: logger.error(f"OpenAI API エラー: lang={yt_lang}, error={e}") raise content = resp.choices[0].message.content try: data = json.loads(content) title = data.get("title", "").strip() desc = data.get("description", "").strip() except json.JSONDecodeError as e: logger.error(f"OpenAI からの JSON パースに失敗しました: lang={yt_lang}, error={e}") logger.error(f"レスポンス内容: {content}") raise return title, desc # ========================= # 動画1本のローカライズ更新 # ========================= def update_video_localizations( youtube, oa_client: OpenAI, video_id: str, ) -> Tuple[List[str], List[str]]: """ 1本の動画について、多言語タイトル/概要を登録する。 戻り値: (成功した言語リスト, 失敗した言語リスト) """ logger.info(f"動画情報取得中: video_id={video_id}") try: res = youtube.videos().list( part="snippet,localizations", id=video_id, ).execute() except HttpError as e: logger.error(f"YouTube API エラー (videos.list): video_id={video_id}, error={e}") return [], list(LANG_CONFIG.keys()) if not res.get("items"): logger.warning(f"動画が見つかりませんでした: video_id={video_id}") return [], list(LANG_CONFIG.keys()) video = res["items"][0] snippet = video["snippet"] localizations = video.get("localizations", {}) base_title = snippet.get("title", "") base_desc = snippet.get("description", "") logger.info(f"元タイトル: {base_title}") logger.info(f"元概要: {base_desc[:60]}...") # 冒頭だけ表示 # デフォルト言語を明示 snippet["defaultLanguage"] = BASE_LANG success_langs: List[str] = [] failed_langs: List[str] = [] # 各ターゲット言語でローカライズ for yt_lang, meta in LANG_CONFIG.items(): if yt_lang == BASE_LANG: continue logger.info(f" ローカライズ開始: {yt_lang} ({meta['name']})") try: loc_title, loc_desc = localize_for_language( oa_client, base_title, base_desc, yt_lang, meta ) except Exception: logger.error(f" ローカライズ失敗: {yt_lang}") failed_langs.append(yt_lang) continue logger.info(f" ローカライズ結果タイトル [{yt_lang}]: {loc_title}") localizations[yt_lang] = { "title": loc_title, "description": loc_desc, } success_langs.append(yt_lang) # YouTube に更新を反映 update_body = { "id": video_id, "snippet": snippet, "localizations": localizations, } try: youtube.videos().update( part="snippet,localizations", body=update_body, ).execute() logger.info(f"動画のローカライズを更新しました: video_id={video_id}") except HttpError as e: logger.error(f"YouTube API エラー (videos.update): video_id={video_id}, error={e}") # 更新自体が失敗したらこの動画は全滅扱い return [], list(LANG_CONFIG.keys()) return success_langs, failed_langs # ========================= # 補助:動画ID読み込み # ========================= def load_video_ids(path: str = "video_ids.txt") -> List[str]: if not os.path.exists(path): logger.error(f"動画ID一覧ファイルがありません: {path}") raise FileNotFoundError(path) ids: List[str] = [] with open(path, encoding="utf-8") as f: for line in f: line = line.strip() if line and not line.startswith("#"): ids.append(line) logger.info(f"動画IDを {len(ids)} 件読み込みました。") return ids # ========================= # メイン # ========================= def main(): youtube = get_youtube_client() oa_client = get_openai_client() video_ids = load_video_ids() total_videos = len(video_ids) total_success = 0 total_failed = 0 for idx, vid in enumerate(video_ids, start=1): logger.info("=" * 60) logger.info(f"[{idx}/{total_videos}] 処理開始: video_id={vid}") success_langs, failed_langs = update_video_localizations( youtube, oa_client, vid ) logger.info(f"[結果] video_id={vid}") logger.info(f" 成功言語: {success_langs}") if failed_langs: logger.warning(f" 失敗言語: {failed_langs}") if success_langs: total_success += 1 else: total_failed += 1 logger.info("=" * 60) logger.info("全動画の処理が完了しました。") logger.info(f" 成功動画数: {total_success}") logger.info(f" 失敗動画数: {total_failed}") logger.info(f" 対象動画数: {total_videos}") if __name__ == "__main__": try: main() except KeyboardInterrupt: logger.info("Ctrl+C を検知したので終了します。") raise SystemExit(1) PythonのエコシステムとLLMの相性は抜群で、API周りの面倒な処理もサクッと実装できました。 YoutubeにもAPIがあったんですね~。GCP経由ですがこのユースケースなら無料です。 ...

2025年12月9日 · 5 分