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経由ですがこのユースケースなら無料です。
効果のほどは?
「タイトルを現地の言葉にすれば、再生数が劇的に伸びるのか?」
正直なところ、現時点ではその効果は未知数です。 ただ、今回の目的は数字を追うことよりも、「各文化圏のユーザーが母国語でアクセスできる状態を作る」 ことにありました。
英語やスペイン語といった主要言語だけでなく、より多くのマイナー言語にも対応させることで、あらゆる文化圏へのアクセシビリティを確保しています。
意外とAPIコストは低い
LLMを使って翻訳を行うと、API利用料(トークン消費)が気になる方もいるかもしれません。
しかし、実際にやってみると意外とAPIトークンは使いません。 動画の中身(音声や映像)を解析するわけではなく、あくまで「タイトル」と「概要欄」というテキストデータの翻訳処理だけなので、非常に軽量です。 もっといえば、DeepLのfreeAPIであれば無料です。機械的な翻訳になるかとは思いますが。
まとめ
これからは、作ったコンテンツがいかに国境を越えられるかが重要になってくる気がします。気がするだけ。 このブログも多言語に自動プッシュしてますからね。。。