TVPulse Python SDK
日本のテレビ放送に関する検索・集計APIを、型付きのPythonモデルで利用するためのSDKです。カスタマーAPIの説明、入力検証、エラー情報、有限リトライをSDKの公開契約として扱います。
インストール
pip install tvpulse==1.0.0
対応するPythonは3.10、3.11、3.12です。APIキーはコードに直接書かず、TVPULSE_API_KEYで渡してください。
export TVPULSE_API_KEY='組織のAPIキー'
カスタマーAPIの公式ドキュメントは、APIドキュメントをご覧ください。
0.xからの移行 (Migrating from 0.x)
PyPIのtvpulse 0.0.0-0.2.0は、本SDKとは別の、アーカイブ済みリポジトリ
(infinimind-inc/tvpulse_python_sdk)由来の旧SDKです。検索(searches)・エクスポート(exports)
APIを含む現行のカスタマーAPIには対応していません。1.0.0はその後継ではなく、
現行のTVPulseカスタマーAPI向けに書き直した別物のSDKです。
- 旧SDKを使い続ける場合は
pip install "tvpulse<1"でピン留めしてください。 - 旧SDKのモジュール構成は本リポジトリの外にあり、対応関係を機械的に検証できないため
網羅的な対応表は提供しません。確認できている範囲では、旧SDKのクライアントに相当するのが
本SDKの
TVPulseClient/AsyncTVPulseClient(下記「同期クライアント」参照)です。 - 移行時の疑問は support@infinimind.io までご連絡ください。
同期クライアント
from tvpulse import SearchTrendRequest, TVPulseClient
request = SearchTrendRequest(
keyword="チーズケーキ",
start_date="2026-05-01 00:00:00",
end_date="2026-05-01 23:59:59",
streams=["cx", "ntv"],
frequency="hour",
)
with TVPulseClient() as client:
response = client.combined.search(request)
print(response.data)
原文検索のキーワードなしモード
SearchRawRequestのkeywordは省略できます。キーワードなしの場合は、streamsとstart_date/end_dateの半開区間(最大24時間)か、正のairing_idのどちらか一つを指定します。キーワードを指定する場合は、streamsと開始・終了日時も必要です。modeはキーワード検索だけで使えます。
cm_filterはinclude(既定)、exclude、onlyから選べます。limitは1〜100件で、応答のnext_page_tokenを次のリクエストに渡すと続きのページを取得できます。検索コンテキストは約2分で失効するため、cursor_expiredの場合は最初のページから再実行してください。
キーワードなし検索の局コードは1〜15件の重複しないサポート対象局です。15局の入力は受け付けますが、標準料金外のためGatewayでcustom_pricing_required(403)になります。14局以下は既存の標準料金で計算されます。
from tvpulse import SearchRawRequest, TVPulseClient
request = SearchRawRequest(
start_date="2026-08-01 00:00:00",
end_date="2026-08-01 01:00:00",
streams=["ntv", "tbs"],
cm_filter="only",
limit=25,
)
with TVPulseClient() as client:
page = client.text.search_raw(request)
if page.next_page_token:
next_page = client.text.search_raw(request.model_copy(update={"next_page_token": page.next_page_token}))
検索定義と有界投影
新しい/v1/api/searchesは、検索条件を軽量なオーナースコープの定義として保存し、必要な投影だけを個別に取得します。作成時は、同じオーナー内で再送を識別する不透明なidempotency_keyを必ず指定してください。
from datetime import datetime, timezone
from tvpulse import SearchDefinitionCreateRequest, TVPulseClient
request = SearchDefinitionCreateRequest(
keyword="コーヒー",
visual_query="コーヒーのパッケージ",
sources=["ocr", "asr", "visual"],
streams=["cx", "ntv"],
start_at=datetime(2026, 5, 1, tzinfo=timezone.utc),
end_at=datetime(2026, 5, 2, tzinfo=timezone.utc),
)
with TVPulseClient() as client:
search = client.searches.create(request, idempotency_key="customer-search-20260501-1")
trend = search.trend(resolution="hour")
breakdown = search.breakdown()
evidence_page = search.evidence(limit=50)
context = search.context(max_chars=5_000)
programs = search.programs()
projection_hints.evidence_limitは定義作成時に宣言する任意の深度で、既定値は50、範囲は1〜100です。定義作成の料金はこの宣言を使います。search.evidence(limit=...)のGETはクレジットを消費せず、要求値が宣言値を超える場合はHTTP 200のまま宣言値までのページを返します。ページの継続では最初の要求と同じlimitを使ってください。これは、定義作成後にGETのlimitだけを増やして追加の深度を取得できない、意図的な契約変更です。
search.evidence()は1ページだけを返します。evidence_page.next_cursorがある場合は、その値を次の呼び出しのcursorに渡し、処理済みページを保持しないでください。サーバーの要求上限は1ページ100件、カーソル256文字です。投影が個別に非同期として受理された場合はHTTP 202とProjectionStatusResponseが返ります。定義が期限切れの場合は安全なTVPulseSearchExpiredError(HTTP 410)が返ります。
Visual ANNはランキングされたvisual_evidenceであり、露出母数、シェア、露出秒数ではありません。検証済みの時間区間をcoalesceしてunionする将来のexposure productだけが、露出秒数を提供できます。
レコード・集計・エクスポート(アルファ版)
/v1alpha1/のレコード関連APIはアルファ版です。契約はリリース間で変わる可能性があります。キーワードなしで、OCR観測レコード(client.text)とASRセグメントレコード(client.voice)をカーソルページングで列挙できます。
- 応答のタイムスタンプはすべてRFC3339の
+09:00(JST)付きです。入力は任意のオフセット付きRFC3339を受け付けます。期間は半開区間[start_at, end_at)です。 - 1ページが課金単位です。
iter_records()はnext_cursorを自動で追跡し、max_recordsで上限を指定できます。カーソルが進まなくなった場合(ライブ端)も安全に停止します。 - すべての応答に
coverage(ストリームごとのhigh_water_markとarchive_floor、番組フィルタ時は解決済み区間)が含まれます。 commercial_statusはcommercial | program | unknownの3値です。CM区間メタデータから判定され、レコード側のフラグは使いません。
from tvpulse import TVPulseClient
with TVPulseClient() as client:
for record in client.text.iter_records(
streams=["ntv"],
start_at="2026-08-13T00:00:00+09:00",
end_at="2026-08-14T00:00:00+09:00",
page_size=1000,
max_records=5000,
):
print(record.observed_at, record.commercial_status, record.text)
aggregates = client.text.aggregates(
streams=["ntv"],
start_at="2026-08-13T00:00:00+09:00",
end_at="2026-08-14T00:00:00+09:00",
granularity="hour",
group_by="stream",
)
集計(aggregates())はgranularity(minute | hour | day | week)とgroup_by(stream | total)を受け付けます。単位はOCR観測数またはASRセグメント数で、モダリティをまたぐ合算はありません。アルファ版ではcommercialはincludeのみサーバーが受理します。
大きな期間はclient.exportsで非同期エクスポートします。dry_run=Trueの見積りが提出時の価格になり、課金は提出時に発生します。状態ポーリングも通常リクエストとして課金されるため、wait()の既定poll_intervalは5秒です。
from tvpulse import ExportCreateRequest, TVPulseClient
request = ExportCreateRequest(
modality="ocr",
streams=["ntv"],
start_at="2026-08-13T00:00:00+09:00",
end_at="2026-08-14T00:00:00+09:00",
idempotency_key="export-20260813-ntv-1",
)
with TVPulseClient() as client:
estimate = client.exports.create(request.model_copy(update={"dry_run": True}))
accepted = client.exports.create(request)
final = client.exports.wait(accepted.export_id, poll_interval=5, timeout=1800)
paths = client.exports.download(accepted.export_id, "./exports", verify_checksums=True)
download()はマニフェストの各パーツ(presigned URL、15分で失効、ポーリングごとに再発行)をディスクへストリーミングし、sha256を検証してからファイルパスの一覧を返します。チェックサム不一致はTVPulseExportIntegrityErrorになります。エクスポートの成果物は7日で失効します。
トレンドカードとウォッチ(アルファ版)
client.trend_cardsは、Xまたはニュースをソースにした自由なトピックの分析カードを生成します。create()は、生成を受理したTrendCardAcceptedResponseまたは、十分に新しいカードを再利用したTrendCardJobResponseを返します。create_and_wait()は受理されたカードだけをポーリングし、キャッシュ済みのHTTP 200は追加の状態確認なしで返します。
client.watchesは、保存したカード条件を読み取り時だけ更新するlazy watchを管理します。latest()の200応答は、新鮮なカード、refreshing=Trueの前回カード、またはcard=Noneとrefresh_deferred_concurrency_limit警告を持つ遅延応答です。初回生成はWatchLatestAcceptedResponseの202です。200応答のwarningsは常に確認してください。
カードの状態確認は通常のリクエストとして課金されます。create_and_wait()のpoll_intervalには余裕を持たせてください。404はTVPulseNotFoundError、409はTVPulseConflictErrorに変換されます。
from tvpulse import TrendCardCreateRequest, TVPulseClient, WatchCreateRequest
with TVPulseClient() as client:
card = client.trend_cards.create_and_wait(
TrendCardCreateRequest(topic="コーヒー", source_type="x", window="weekly"),
poll_interval=5,
)
watch = client.watches.create(
WatchCreateRequest(topic="コーヒー", source_type="x", window="weekly"),
)
latest = client.watches.latest(watch.watch_id)
print(card.status, latest)
非同期クライアント
import asyncio
from tvpulse import AsyncTVPulseClient, SearchRawRequest
async def main():
request = SearchRawRequest(
keyword="ニュース",
start_date="2026-05-01 00:00:00",
end_date="2026-05-01 00:30:00",
streams=["ntv"],
)
async with AsyncTVPulseClient() as client:
response = await client.voice.search_raw(request)
for hit in response.data:
print(hit.text)
asyncio.run(main())
公開操作の対応表
| Gateway操作 | SDKの呼び出し | 応答モデル |
|---|---|---|
POST /v1/api/combined/search |
client.combined.search() |
CombinedSearchResponse |
POST /v1/api/combined/search/stream |
client.combined.search()が条件に応じて自動選択 |
CombinedSearchResponse |
POST /v1/api/combined/summary |
client.combined.summary() |
SummaryResponse |
POST /v1/api/combined/nlp |
client.combined.nlp() |
CombinedNLPResponse |
POST /v1/api/combined/related-keywords |
client.combined.related_keywords() |
RelatedKeywordsResponse |
POST /v1/api/text/search/raw |
client.text.search_raw() |
TextSearchResponse |
POST /v1/api/voice/search/raw |
client.voice.search_raw() |
VoiceSearchResponse |
POST /v1/api/searches |
client.searches.create() |
product-shaped search handle |
GET /v1/api/searches/{search_id} |
client.searches.get() |
product-shaped search handle |
GET /v1/api/searches/{search_id}/trend |
search.trend() |
TrendProjectionResponseまたはProjectionStatusResponse |
GET /v1/api/searches/{search_id}/breakdown |
search.breakdown() |
BreakdownProjectionResponseまたはProjectionStatusResponse |
GET /v1/api/searches/{search_id}/evidence |
search.evidence() |
EvidenceProjectionResponseまたはProjectionStatusResponse |
GET /v1/api/searches/{search_id}/context |
search.context() |
ContextProjectionResponseまたはProjectionStatusResponse |
GET /v1/api/searches/{search_id}/programs |
search.programs() |
ProgramsProjectionResponseまたはProjectionStatusResponse |
GET /v1alpha1/trend-watch/catalog/reports |
client.trend_watch.list_reports() |
TrendWatchReportListResponse |
GET /v1alpha1/trend-watch/catalog/reports/{report_id} |
client.trend_watch.get_report() |
TrendWatchReportDetail |
POST /v1alpha1/trend-watch/catalog/reports/exports |
client.trend_watch.export_reports() |
TrendWatchExport |
POST /v1alpha1/visual-search/searches |
client.visual.search() |
VisualSearchResponseまたはVisualSearchAcceptedResponse |
POST /v1/api/credits/estimate |
client.credits.estimate() |
CreditEstimate |
POST /v1/api/credits/estimate/rows |
client.credits.estimate_rows() |
CreditEstimateRows |
GET /v1/api/credits |
client.credits.balance() |
CreditBalance |
GET /v1alpha1/text/records(アルファ版) |
client.text.records() / client.text.iter_records() |
TextRecordsPage / TextRecordのイテレータ |
GET /v1alpha1/voice/records(アルファ版) |
client.voice.records() / client.voice.iter_records() |
VoiceRecordsPage / VoiceRecordのイテレータ |
GET /v1alpha1/text/aggregates(アルファ版) |
client.text.aggregates() |
RecordsAggregatesResponse |
GET /v1alpha1/voice/aggregates(アルファ版) |
client.voice.aggregates() |
RecordsAggregatesResponse |
POST /v1alpha1/exports(アルファ版) |
client.exports.create() |
ExportAcceptedResponseまたはExportEstimate |
GET /v1alpha1/exports(アルファ版) |
client.exports.list() |
ExportListResponse |
GET /v1alpha1/exports/{export_id}(アルファ版) |
client.exports.get() / client.exports.wait() / client.exports.download() |
ExportStatusResponse / ダウンロード済みファイルパス |
POST /v1alpha1/trend-cards(アルファ版) |
client.trend_cards.create() / client.trend_cards.create_and_wait() |
TrendCardAcceptedResponseまたはTrendCardJobResponse |
GET /v1alpha1/trend-cards/{card_id}(アルファ版) |
client.trend_cards.get() |
TrendCardJobResponse |
POST /v1alpha1/watches(アルファ版) |
client.watches.create() |
WatchResponse |
GET /v1alpha1/watches(アルファ版) |
client.watches.list() |
WatchListResponse |
GET /v1alpha1/watches/{watch_id}/latest(アルファ版) |
client.watches.latest() |
WatchLatestResponseまたはWatchLatestAcceptedResponse |
DELETE /v1alpha1/watches/{watch_id}(アルファ版) |
client.watches.delete() |
None |
Published search projections are synchronous. search.projection_status() remains compatibility-only until a real producer exists; no supported production flow creates a projection ID, and the status route is not published in customer OpenAPI.
ビジュアル検索が受付応答になった場合は、公開操作数に含まれない状態確認用のclient.visual.get_search()を使えます。受付から完了までをまとめる場合はclient.visual.search_and_wait()を使ってください。状態確認のパスは現在のゲートウェイ実装にありますが、公開OpenAPIでは非表示です。
クレジットの見積もりと残高
実行前に消費クレジットを確認し、残高と突き合わせられます。どちらの呼び出しも0クレジットです(レート制限は消費します)。残高が0のキーやAPIサブスクリプションを持たないキーでも呼び出せます。
from tvpulse import SearchTrendRequest, TVPulseClient
request = SearchTrendRequest(
keyword="コーヒー",
start_date="2026-05-01 00:00:00",
end_date="2026-05-02 00:00:00",
streams=["ntv"],
)
with TVPulseClient() as client:
est = client.credits.estimate("/v1/api/combined/search", request)
if est.balance.sufficient:
response = client.combined.search(request)
bal = client.credits.balance()
print(est.credits, est.outcome, est.factor_revision, bal.total_credits)
見積もりは実行時とまったく同じ計算式で計算されるため、同一のリクエストボディ・同一の料金係数リビジョンなら実際の請求と一致します。応答のfactor_revisionが、どの料金表で計算されたかの証明です。outcomeが"enterprise_review"の場合、そのワークロードはEnterprise/個別見積もりの対象です。実行時には403(custom_pricing_required)で拒否されクレジットは消費されないため、creditsは0で、flagsにcustom_pricing_required:*(対象の条件)が入ります。
estimate_rows: 検索結果行数の見積もり
client.credits.estimate_rows()はPOST /v1/api/credits/estimate/rowsを呼び出し、検索を実行せずに完全一致する行数と取得クレジットを返します。こちらも0クレジットのプリフライトです。エンドポイントは/v1/api/searchesのみを受け付けます。
from datetime import datetime, timezone
from tvpulse import SearchDefinitionCreateRequest, TVPulseClient
request = SearchDefinitionCreateRequest(
keyword="コーヒー",
sources=["ocr", "asr", "visual"],
streams=["cx", "ntv"],
start_at=datetime(2026, 5, 1, tzinfo=timezone.utc),
end_at=datetime(2026, 5, 2, tzinfo=timezone.utc),
)
with TVPulseClient() as client:
rows = client.credits.estimate_rows("/v1/api/searches", request)
print(rows.total_rows, rows.retrieval, rows.outcome)
total_rowsはOCR/ASRのテキストソースに一致する行数の合計です。ビジュアル行は行数に含まれず、visual_excludedがtrueでflagsにvisual_rows_not_countedが入ります。retrievalは完全な結果セットを取得するためのクレジットで、フラットな1クレジットの継続ページを含みます。outcomeが"enterprise_review"の場合、宣言されたリクエストまたは取得のページング上限を超えるワークロードで、Enterprise/個別見積もりの対象です。declared_request_creditsは宣言されたリクエスト自体のクレジットです。
残高応答のbucketsは種別ごとの内訳です。プラン付帯の月次クレジット(allowance)は毎月リセットされ繰り越されません。購入済みクレジットパック(pack)は失効しないためexpires_atはnullです。
入力と応答
日時は操作ごとの契約に合わせて検証します。通常の検索は日本時間のタイムゾーンなしYYYY-MM-DD HH:mm:ss、ビジュアル検索はオフセット付きRFC3339です。大文字の検索モードANDとORは互換入力として受け付けますが、送信値はandとorに正規化します。
辞書を渡す場合も、型付きリクエストモデルを通した場合と同じ検証を行います。応答は型付きモデルで返り、未知の応答フィールドも保持します。辞書が必要な連携では、各応答のto_raw()を使ってください。
エラーとリトライ
次の例外を公開しています。
TVPulseValidationError: 400または422TVPulseAuthError: 401TVPulseAuthorizationError: 権限不足の403TVPulseNotFoundError: 404(例:export_not_found。互換性のためTVPulseUnknownServerErrorのサブクラス)TVPulseConflictError: 409(例: エクスポートの同時実行上限。互換性のためTVPulseUnknownServerErrorのサブクラス)TVPulseSearchExpiredError: 期限切れの検索定義(410、error_code=search_expired)TVPulseInsufficientCreditError:error_code=insufficient_creditの403TVPulseRateLimitError: 429TVPulseBackendError: 5xxTVPulseUnknownServerError: 契約外のステータスまたは解釈できない応答TVPulseTimeoutError: タイムアウトTVPulseNetworkError: 接続などのネットワークエラーTVPulseExportIntegrityError: エクスポートパーツのsha256チェックサム不一致
APIエラーにはstatus_code、error_code、correlation_id、retry_after、retryable、bodyが含まれます。旧SDKとの互換性のためcodeとrequest_idも利用できます。
既定では最大2回の追加試行を行います。ネットワークエラー、429、5xxだけを対象にし、429と5xxではRetry-Afterを優先します。それ以外の4xxは再試行しません。必要に応じてmax_retries=0、retry_backoff=秒数で調整できます。
from tvpulse import TVPulseClient
with TVPulseClient(max_retries=2, retry_backoff=0.5) as client:
response = client.text.search_raw(request)
サンプル
examples/voice_search.py: 音声検索examples/combined_search.py: 集計検索と型付き応答examples/searches.py: 検索定義と有界投影examples/text_records.py: レコード列挙とエクスポート(アルファ版)
サンプルの日付は契約形式を示す固定例です。利用時は、利用可能な放送期間に置き換えてください。
Release files for tvpulse 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tvpulse-1.0.0.tar.gz | 39.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tvpulse-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.7 kB
Release files / tvpulse-1.0.0.tar.gz
| Download URL | tvpulse-1.0.0.tar.gz |
|---|---|
| Size | 39.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
401d590284f73ad9b5b5ac8df43b3c153ad2868ed326ffcf487af39d65cf9eae
|
|
BLAKE2b-256 checksum How to use checksums |
a94dbe07dab3e1d6652f58c87753a65a3da701c3f4b3ab089fec5875e077260d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / tvpulse-1.0.0-py3-none-any.whl
| Download URL | tvpulse-1.0.0-py3-none-any.whl |
|---|---|
| Size | 40.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f8964df82a73011b88b11afc27d50c438fd38f493e3b23512c5bc42f51b828eb
|
|
BLAKE2b-256 checksum How to use checksums |
882949e7b58a1ecca2c094f042003d620a700b5788d65313dbab03d737658e1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|