Skip to content
Skillv1.0.0

vlogall

Complete self-contained pipeline for producing time-travel history vlog episodes as scene-by-scene JSON for Higgsfield Seedance (8-12 scenes x 10s, 9:16, vertical selfie vlog), including the source-vi

by noellesugar99(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from noellesugar99/higgsfield-runner (extras/vlogall/SKILL.md). Install upstream with npx skills add noellesugar99/higgsfield-runner --skill vlogall. Copyright stays with the author.

時代タイムトラベルVlog 完全版(vlogall)

参考動画の内容を、一人称Vlogのシーン別JSONに変換し、Higgsfield Seedanceへそのまま貼れる形(コピーボタン付きHTML+xlsx)で出す。

このskillは単体で完結している。 材料の作り方・書式・見本・文体・考証・検査スクリプト・ビルダーのコード、全部この中にある。外部ファイルは要らない。


0. 設計原則

過去の失敗は全部「ルールを守り忘れた」ではなく「守れたかどうかを誰も判定できなかった」ことが原因だった。

原則1:正解を持つのはモデルの記憶ではなく、台帳(LEDGER)と検査スクリプト。 原則2:検査が通っていない出力は、絶対にユーザーへ見せない。 原則3:文章で書いても守れないルールは、機械が判定できる形に変換してから書き始める。 原則4:ルールを1つ増やしたら、必ず既存の全話へ遡って掛け直す。 原則5:自分の出力について「必ず入っています」と書く前に、その場でデータを数える。 書いた記憶と、入っている事実は別物。 原則6:音声として読まれてよいテキストは、JSONの中に一箇所だけ・かな書きだけで置く。 表示用(ルビ付き)と音声用を同じJSONに同居させない。EP97で、ルビ付きセリフを含むJSONを渡した結果、モデルが漢字の読みとルビを両方発音した(身体《からだ》→「しんたいからだ」)。1回の生成は有料。二重読みは構造で殺す。

一発で完璧になることは期待しない。PASS 0〜7 を順に回し、GATE が exit 0 になるまでループする。 トークンを使ってでもループを回すのがこのskillの仕事。


1. 出力契約(変えない)

項目
1話 8〜12シーン × 10秒(80〜120秒)。12は上限であって目標ではない
比率 9:16(Higgsfieldの既定は16:9なので毎回変える)
モデル Higgsfield Seedance 2.5、Unlimited mode ON。web版でのみ無制限。MCP経由はクレジットを消費するので使わない
クリップ長 10秒固定(他の選択肢が無い)。内部を 0-3s / 3-7s / 7-10s の3ビートに割る
プロンプト言語 英語(Seedance用)
会話・ドキュメント ユーザーの言語(既定:日本語)
コピー形式 JSONのままJSON.stringify(o,null,2)平文化・フィールド連結は禁止
セリフ JSONに入る発話は audio.speech_script 一箇所だけ。かな書きのみで、漢字と 《》 は1文字も入れない。 ルビ付きの表示用セリフ(jpoth)はxlsxとHTML専用で、JSONへは絶対に入れない(入れると漢字とルビが二重に発音される)
文字 画面内の文字・数字は編集で載せる。Seedanceには絶対に生成させない
ファイル 範囲ごとに1組だけ。 20話ごとに1ファイルへ統合し、旧版は中身を差し替えて無効化する

2. PASS 0 — 材料をそろえる

参考動画がある場合、まず動画を解析して台帳の素を作る。 動画を直接見られない環境なら、下のプロンプトを外部のマルチモーダルAIへ渡して結果を受け取る。

解析依頼プロンプト(そのまま渡す)

あなたは映像の内容記録係です。これから短尺の縦型動画を渡します。
見えた事実だけを、次の3ブロックに分けて日本語で出力してください。

厳守
- 画面で物理的に観察できることだけを書く。推測・感想・演出意図は書かない。
- 判断できないものは「不明」と書く。絶対に埋めない。
- 画面の文字と音声のセリフは、元の言語のまま一字一句そのまま写す。翻訳しない。
- 数字(値段・分量・時間・回数・人数)は必ず拾う。呼称・固有名も必ず拾う。

出力
## 基本
URL / 再生数 / 長さ(秒)

## 画像で見えたもの(登場順)
1. 〜  ← 画として映った 物・工程・場所・人 を1項目1行で、登場順に全部。
   工程は「◯◯を作る」とまとめず、1手順1行に割る。

## 字幕・ナレーションが伝えていた内容
1. 〜  ← 言っていた内容を順に。数字と呼称は落とさない。

## Vlog化するときに落としてはいけない内容
- 移動する場所を全部挙げる。
- 現代と違う点(今と逆・今はない・今より高い/安い)を全部挙げる。
- 誰が何をどう食べていたかを書く。

台帳(LEDGER)を作る

シーンを1行も書く前に、上の「画像で見えたもの」へIDを振ってファイルにする。

[{"id":"L01","episode":"EP03","item":"猪を雪道で担いで運び込む"},
 {"id":"L02","episode":"EP03","item":"「山くじら」の看板"},
 {"id":"L03","episode":"EP03","item":"猪鍋。味噌を鍋肌に塗ってから煮る"}]

台帳に必ず入れるもの — 画として映った物・工程・場所・人/数字/呼称/現代との違い/食べている人と食べ方

この台帳がその話の唯一の正解。 自分の知識で足すのは台帳を全部消化したあとだけ。参考動画が複数ある回は項目を合算する。台帳を作ったらユーザーに見せて確認を取る。ここで抜けを潰すのが一番安い。

❌ 過去の失敗:台帳を作らずタイトルとテーマから書いたので、ある回は3品中1品しか入らず、別の回は3品とも別物になり、また別の回は動画の核(製造工程)が丸ごと消えた。


3. パイプライン

PASS 1 — 尺を台帳から決める

シーン数を先に決めてから内容を埋めるのは禁止。

参考チャンネル50本の実測:全部 40.6〜59.5秒、60秒超えは0本、平均49.8秒。 映像項目は1本あたり中央値14、1項目3.3秒。こちらは10秒の実写なので1シーンで元動画の2〜2.5項目ぶんを運べる。

元動画の項目数(中央値) 目安シーン数
選型(3選・4選) 14 8〜9 80〜90秒
単品型(作り方・事件) 14 9〜10 90〜100秒
セットログ型(一日ルーティン) 17 10〜12 100〜120秒

上限 = 台帳の項目数 × 0.8。 13項目なら10シーンまで。超えたらどこかが水増し。

PASS 2 — 型とルートを決める

セットログ型 ストーリー調
時間 一日の時刻で進む 出来事で進む
移動 同じ職場に留まる 場所を渡り歩く
特定の職の視点がある 訪問者の視点
対立 なし 禁忌・身分・値段の壁がある

2つ以上がセットログ側ならセットログ型(時刻カード駆動、視点はその職の人)。毎回同じ型にしない。目安は3話に1話。

セットログ型は始まりの時刻に戻って閉じる。 起床で始めたら就寝で終える。途中の場面で終わらせない。

❌ 過去の失敗:「居酒屋店主の一日ルーティン」を客視点のストーリー調で作り、店主の一日という核を丸ごと落とした。参考素材が「◯◯の一日」ならセットログ型。

ROUTE表:場所の数はシーン数の半分以上同じ場所は最大2連続まで。 表を先に書いてからシーンを書く。

PASS 3 — シーンを書く

第5章の形式で書く。各シーンに必ず covers(台帳ID)を付ける。

PASS 4 — GATE を実行(第8章)

exit 0 になるまで PASS 3 に戻って直す。何周してもよい。落ちたまま先へ進まない。

PASS 5 — 台帳の逆照合(人力)

GATEが通っても、台帳を上から読み直して各項目が「本当にその意味で」入っているかを1行ずつ確認する。必ずこの3列で出す。

台帳ID 元素材の内容 本編での扱い
L05 鹿を薄切りにして紅葉の形に並べた皿 ✅ S6 で皿の形まで再現
L08 反本丸という薬名で献上 ⚠️ 箱は出るが薬名が出ていない → S11に追加

機械で判定できない5項目も目視で見る。

  1. 豆知識が一般論になっていないか。 「◯◯は大切にされた」のような文は書き直す
  2. 工程が1シーンに潰れていないか。 複数手順だったものが一行になっていたら台帳IDを分割して割り直す
  3. 貪り食うシーンが本当に貪っているか。 座って眺めている・上品につまんでいるだけになっていないか
  4. 無音で見て70%伝わるか。 silent_test_beat だけを縦に読んで筋が追えるか
  5. 削れるシーンがないか。 「移動しているだけ」「眺めているだけ」「同じ豆知識の言い換え」は削る。尺が短くなるのは正しい結果

PASS 6 — ビルドと書き出し(第9章)

1範囲1組。 作り直したら旧ファイルの中身を「統合されました/古い版です」に差し替える(削除できない環境が多い)。

PASS 7 — 10話ごとの再照合

10話たまるごとに PASS 5 をまとめてやり直す。新しい話を作るより既存の照合が優先。

PASS 8 — 生成後の音声検査(公開前に毎回)

入力が完璧でも、モデルが台詞を途中から再スタートしたり(EP97 S06)、複合固有名詞を割って誤読したり(本所割下水→「ほんしょわりがすい」)する事故は入力側では防げない。生成された各クリップの音声を Whisper 等で文字起こしし、speech_script(話者タグと句読点を除いたかな)と突き合わせる。

  1. 連続反復:同じ文節が2回続いたら不採用
  2. 固有名詞の不一致:PRONOUNCE 辞書の読みと違ったら不採用
  3. 台本に無い語の追加:不採用

不採用のテイクは同じJSONで再生成する。過去に正常だったシーンの台詞内容は変えない。 全話へ遡って掛け直すのは音声の経路(speech_script 化)と検査だけ。


4. LOCK文字列(一字一句このまま使う)

コードでは vlog_build.py の定数として持つ(第9章)。意味は次のとおり。

名前 使いどころ
NOELLE front_cam_self のとき。右3分の1に立ち、顔でフレームを埋めないという枠の契約
COMPANION companion_handheld のとき。同行者が撮影し、撮影者と機器は画面外。両手を使える
PROPPED propped_static のとき。顔は出ず両手が空く。1話に最大2回
REAR rear_cam のとき。本人がスマホを持ち、顔を映さず前方の対象を主役にする
VLOG_RULE 全シーン。三人称カメラの存在を否定し、自由な手は1本だけと宣言する
DEVOUR / DEVOUR_KUSHI 貪り食うシーン。現地の人にだけ使う。器物と串物で使い分ける
ANDON / KATOU / CHOCHIN / SHOKUDAI 照明。裸の lamp は絶対に書かない
NO_IP 全シーン。文字生成の禁止+時代の禁止列挙

「美味しそうに見える」6条件(DEVOURの中身の根拠)

食べまくり漫画から抽出したもの。貪り食うシーンを書くとき、この6つのうち最低4つを満たす。

  1. 両手が食べ物に触れている(器を掴む、持ち上げる、傾ける)
  2. 飲み込む前に次へ手が伸びている(連続性が食欲を伝える)
  3. こぼれても拭かない(行儀を捨てるほど旨い)
  4. 頬が膨らむ/喉が動く(咀嚼ではなく嚥下を描く)
  5. 前傾して肩が器を囲う(他人に取られない距離)
  6. 食後の即立ち上がり(労働の合間であることが伝わる)

5. エピソードの書式(この形で書けばビルダーがそのまま処理する)

場所(L)と登場人物(C)は話の頭で一度だけ定義し、以後はキーで呼ぶ。 ビルダーが visual_prompt

L[loc] + " " + C[cast]... + " " + NOELLE(またはPROPPED) + " " + act

の順で毎回まったく同じ文字列から組み立てる。これが場所と人物の継続性を担保する唯一の仕組みなので、同じ場所を別の言葉で書き直してはいけない。

登場人物は顔で描写しない。 年代・服装・持ち物・癖で固定する。顔の描写は参照画像と喧嘩する。

完全な見本(2シーン。これをコピーして増やす)

# -*- coding: utf-8 -*-
"""1話ぶんの完全な見本。この形式で書けば vlog_build.py がそのまま処理する。"""
from vlog_build import CHOCHIN, KATOU, DEVOUR_KUSHI
TITLE = "江戸編 見本|EP38 揚げ物"
CUTS = {}            # {"EP38":[3,7]} 圧縮で削るシーン番号
ORDER = {}           # {"EP38":[1,2,4,3,...]} 場所3連続を崩す並べ替え
DEVOUR_EXEMPT = {}   # {"EP10":"将軍の一日。食事が構造上ない"} 理由必須

EPISODES = [dict(
 ep="EP38",
 title="【油は使い回し】江戸の揚げ物屋台が、現代のジャンクフードそのものだった",
 fmt="story",                      # story | set_log
 season="early autumn, eighth month, Bunsei era",
 job=None,                         # セットログ型なら職名
 # --- 場所は話の頭で一度だけ定義し、以後 loc= で呼ぶ(継続性の要)---
 L={
  "YATAI_YORU":"A night food stall on a busy street, a shallow iron pan of dark oil set over charcoal at waist "
   "height, skewers standing upright in a jar, a tall glazed pot of thick dark dipping sauce beside it, standing "
   "customers pressed close, no seats of any kind. "+CHOCHIN,
  "AGEYA_CHUBO":"The back kitchen of a frying shop late at night, three large earthenware oil jars in a row, a "
   "straining cloth stretched over a frame, the oil in the jars ranging from pale gold in the first to almost black "
   "in the last, soot on every surface. "+KATOU},
 # --- 登場人物も一度だけ定義。顔ではなく衣装と持ち物で固定する ---
 C={
  "NINSOKU38":"A labourer in his thirties, bare shoulders under an open workman's jacket, a sweat towel round his "
   "neck, coins already in his fist before he has finished the last one",
  "AGEYA_P":"A frying-shop owner in his fifties, hands permanently darkened, holding the straining cloth"},
 S=[
  dict(n=1, loc="YATAI_YORU", cast=["NINSOKU38"], mode="front_cam_self",
   role="タレに浸して立ち食い", covers=["L03","L04"],
   eats="local", dev="devouring",
   hands="左手=スマホ/右手=空き",
   b=["揚がった串を、そのまま背の高い壺の濃いタレへ沈める",
      "人足が受け取って、立ったまま串から直接かじりつく",
      "噛み終わる前にもう次の銭を出している"],
   act="The finished skewer is dropped whole into a tall glazed pot of thick dark sauce and pulled out coated. "
       "A labourer takes it and eats standing, straight off the skewer. "+DEVOUR_KUSHI+
       " There is nowhere to sit anywhere at the stall.",
   fact="串一本四文。座る場所は無い",
   gap="今は座って食べるが、江戸は全員立ち食い",
   sil="椅子が一つも無い。全員立って食べている",
   prop=None,
   cam="eye-level / handheld selfie, she at frame right, the sauce pot and the eating labourer filling frame left / medium-wide selfie / 24mm front / medium",
   lit=CHOCHIN+" / low front / very warm / high",
   food="a skewer lifted dripping from near-black sauce, glossy coating, sauce running down the wooden stick",
   mi="high",
   amb="loud chewing, sauce dripping, oil, crowd",
   jp="(ノエル)座《すわ》るとこ、無《な》いんだ",
   tts="すわるとこ、ないんだ",
   oth=None),
  dict(n=2, loc="AGEYA_CHUBO", cast=["AGEYA_P"], mode="front_cam_self",
   role="黒くなった油", covers=["L12"],
   eats="none", dev="none",
   hands="左手=スマホ/右手=壺の縁を指でなぞる",
   b=["厨房の奥に油壺が三つ並んでいる。左から順に色が濃くなっている",
      "一番右は、もう黒くてどろりとしている",
      "職人が布で漉して、また使う。彼女が三つを順に指でなぞる"],
   act="Three large earthenware oil jars stand in a row in the back kitchen. The oil goes from pale gold in the "
       "first, to brown in the second, to almost black and visibly thick in the third. The owner strains it through "
       "a cloth and pours it back to be used again. She runs a finger along the three rims in order.",
   fact="油は布で漉して三度も四度も使い回した",
   gap="今は一回で捨てるが、江戸は真っ黒になるまで使う",
   sil="三つの壺で色が段階的に黒くなっている",
   prop=None,
   cam="eye-level / handheld selfie, she at frame right, the three jars filling frame left / medium-close selfie / 24mm front / medium",
   lit=KATOU+" / low side / very warm / high",
   food="three jars of oil, pale gold to brown to near-black and thick, sediment at the bottom of the last",
   mi="low",
   amb="oil pouring slowly, cloth straining, a quiet back room",
   jp="(ノエル)これ、まだ使《つか》うの",
   tts="これ、まだつかうの",
   oth=None),
 ])]

各キーの必須条件

キー 条件
covers 空にできない。 台帳IDを1つ以上
mode front_cam_self / companion_handheld(1話0〜2) / propped_static(1話最大2) / rear_cam
hands 必ず「片手=スマホ」を書く。 自由な手は1本だけ。手が3本になる記述は禁止
b ちょうど3つ。0-3s / 3-7s / 7-10s に対応
fact 空にできない。 1話に3個以上は数字を含む
gap 空にできない。「今は◯◯だが、当時は△△」の形
eats local / noelle / nonenoelle は1話に最大2回
dev none / tasting / devouringdevouringeats="local" のときだけ。1話に2回以上
sil 音を切って何が見えるか。 セリフに依存する説明を書かない
cam 角度 / 動き / ショット / レンズ / 被写界深度 をスラッシュ区切りで5つ
lit 照明LOCK / 方向 / 色温度 / コントラスト
tc セットログ型のみ ("暮六つ","18:00")

カメラ

許可handheld selfie / handheld hover / she physically turns the phone / she flips the phone / her arm bends in

禁止pan / tracking shot / dolly / crane / drone / static wide / follows her from behind / the camera moves(三人称の存在を示唆するもの全部)

禁止表現her face fills the frame / MONEY SHOT → この2つを書いた瞬間、料理と現地の人が物理的に画面から追い出される。 実食シーンの見出しは EATING SHOT - prioritise the food and the eating, not her face. にする。

❌ 過去の失敗:「実食=最重要=顔をアップに」というテンプレを持っていたため、全話の一番大事なシーンで料理も現地人も消えた。実食シーンの主役は口であって顔ではない。

5.1 カメラ方式・手・LOCK(G17)

シーンごとにカメラ方式を一つだけ選ぶ。方式を選んだら、vlog_rulehandsvisual_promptbeats の全てを同じ方式にする。自撮りで両手作業を無理に行わせない。物理的に両手が必要な場面は companion_handheld または propped_static を優先する。

front_cam_self(自撮り)

{
  "camera_mode": "front_cam_self",
  "vlog_rule": "彼女が左手でスマホを構えている。自由な手は右手一本だけ",
  "hands": "左手=スマホ/右手=一つの小道具"
}
  • 主人公がスマホを持ち、自由な手は一本だけ。
  • 右三分の一の自撮り構図、左手の前腕、電話本体は画面外を維持する。
  • 右手に割り当てる小道具は一つだけ。器と匙など二つを同時に持たせない。

companion_handheld(同行者の手持ち撮影)

  • 同行者がスマホまたは小型撮影機器を持つ。撮影者と機器は画面外。
  • 主人公の両手が自由で、揺れは画面外の同行者の手から発生する。
  • 自撮り用の右三分の一構図は必須ではない。同行者を江戸の登場人物として生成しない。
  • 左手と右手の担当物を別々に書き、受け渡し・持ち替えの順番をビート内で追跡する。

標準記述:

{
  "camera_mode": "companion_handheld",
  "vlog_rule": "同行者がスマホで撮影している。撮影者とスマホは画面に映らない",
  "hands": "左手=椀の底を支える/右手=竹の匙"
}

生成文に必ず含めるLOCK:

A companion films the recurring host in a single continuous handheld shot.
The companion stays behind the recording device and remains outside the image.
Both of the host's hands are free and visible.
Her LEFT hand supports the bowl from underneath while her RIGHT hand handles only the bamboo scoop.
Natural handheld movement comes from the unseen companion.

propped_static(置き撮り)

  • 撮影機器は固定物へ置く。固定物自体は画面へ生成しない。
  • 主人公の両手が自由で、カメラ位置・角度・高さは10秒間変えない。
  • 手持ちの揺れ、同行者の操作、構図変更を指定しない。

標準記述:

{
  "camera_mode": "propped_static",
  "vlog_rule": "スマホは固定されている。構図は変わらず、彼女の両手は自由",
  "hands": "左手=器/右手=調理道具"
}

rear_cam(背面カメラ)

  • 主人公本人がスマホを持ち、自由な手は一本だけ。
  • 主人公の顔は映さず、前方の対象を主役にする。両手作業には使わない。

方式の選択基準

次のいずれかに該当するなら、まず companion_handheld を検討する:両手で器と匙・箸を使う、布を絞る・こねる・包む、重い桶・鍋・箱を扱う、全身の所作や着替え・履物を見せる、主人公と現地人物の受け渡しを両手で見せる、器を傾けて粘度や流れを見せる。

歩きながら場所を紹介する、右手一本で指差す、小さな物を一つだけ持つ、顔の反応と背景を同時に見せる場面は front_cam_self を優先する。固定した同じ場所で両手の工程を見せるなら propped_static を使う。

他撮りと置き撮りは、映画的な豪華さではなく、物理的に成立する手と小道具の関係を作るために選ぶ。companion_handheld は1話0〜2本、propped_static は1話最大2本。カメラ方式を10秒のビート途中で切り替えない。

G17 自動検査

FAILcompanion_handheld なのに hands にスマホがある/visual_promptshe holds her phone がある/camera IS her phone客観カメラは存在しないher own arm shake が残る/front_cam_self なのにスマホ以外の両手作業や「両手で作業」がある/propped_static なのに handheldoperator movementher own arm shake がある/1話の同行者撮影が3本以上/ビート内で方式を切り替える。

WARNING:他撮りなのに右三分の一構図を強制している、撮影者の位置が不明、両手の持ち替え順が不明、置き撮りなのに主人公がフレーム外へ大きく移動する、約10シーン全てが自撮りで両手作業が複数ある。

PASS:1シーン1方式、方式対応LOCKのみ、front_cam_selfrear_cam は自由な手が一本、他撮りと置き撮りは両手が自由、左右の手と各小道具を全ビート追跡できる、他撮り0〜2本、両手が必要な重要シーンは他撮りまたは置き撮り。

甘酒の粘度のように、左手で椀を支え右手で竹匙を扱う場面は companion_handheld にする。左手で椀をゆっくり傾ける→右手の匙ですくう→椀の上で匙を見せる、の順を一つの10秒シーン内で完結させる。


6. 文体ガイド

act(英語・映像の指示)

  • 観察できる動作だけを書く。 評価・感情・雰囲気の形容詞を書かない(beautiful delicious atmospheric は禁止)
  • 3〜5文。3つのビートに対応させる
  • 主語は She / He / The crowd / 物
  • 数と量を必ず具体で書くthree jars about ten seconds half a radish
  • 否定形で世界を締める(There is nowhere to sit no ice anywhere in the market)。これは考証の固定にも効く
  • 料理の見た目は act に書かず food に分ける

jp(表示用)と tts(音声用)— 役割を絶対に混ぜない

EP97の事故:ルビ付きセリフがJSONに入っていたため、モデルが漢字の読みとルビを両方発音した(身体→「しんたいからだ」、絵手本→「えてほんえてほん」)。だから今は二層構造にする。

行き先 書き方
jp / oth(表示用) xlsxとHTMLだけ。JSONには入らない 全漢字ルビ付き 漢字《かんじ》。話者名の漢字にもルビ
tts / oth_tts(音声用) ビルダーが audio.speech_script としてJSONに入る唯一の発話 かなのみ。手で書く。自動かな変換は禁止(過去に「おぼえるえてほん」のような壊れた境界を作った)

tts の書き方:

  • 読点「、」で自然な文節に区切る。 かなのべた書きはモデルが区切りを誤る
  • 固有名詞は vlog_build.py の PRONOUNCE 辞書の読みをそのまま使う。 一字も変えない。新しい固有名詞は先に辞書へ PRONOUNCE.update({...}) で登録してから使う
  • 数字もかなで書く(「じゅうろくもん」「よんごう」)。算用数字・漢数字は禁止
  • 1台詞(1話者ぶん)は目安30字、上限40字。 超えるとビルダーが止まる。10秒に収まらない台詞は反復・再スタート事故の温床(EP97 S10)
  • 相手役の oth_tts は話者タグごとかなで((ほくさい)あきは、つぎのかみをおくばしょだ

内容のルール(jptts は同じ内容の表示違い):

  • 一言か二言。 説明しない。見えたものへの反応だけ
  • 驚きの前に …… を置いて間を作る
  • 省略形と体言止め(「つめた」「しょっぱ」「でかっ」「無理《むり》でしょ」)
  • 語尾は疑問か気づき(「〜なの?」「〜んだ」「〜じゃん」)
  • 数字は必ず口に出す
  • 1話に2〜3シーンは jp=""(無言)にしてよい。置き撮りシーンは基本無言

良い例/悪い例:

(ノエル)そばより高《たか》い。卵《たまご》のほうが (ノエル)江戸時代では卵は高級品だったんですね
(ノエル)……中《なか》、普通《ふつう》。普通《ふつう》なんだ (ノエル)驚いたことに中身は普通のゆで卵でした
(ノエル)二尾目《にひきめ》いった (ノエル)彼はもう二尾目を食べ始めています

7. 時代パック(江戸)

照明 — 裸の lamp は絶対に書かない

場面 使う定数
屋内・部屋 ANDON(行灯)
屋内・台所や作業場 KATOU(瓦灯)
屋外・街路 CHOCHIN(提灯)
城・大名屋敷 SHOKUDAI(燭台と和蝋燭)

置換辞書(左辺は書いてはいけない)

書きがち 正しい
lamp / oil lamp / lamplight 行灯・瓦灯・提灯・燭台のいずれかを特定
mirror a polished bronze hand mirror
spoon a small bamboo scoop
table(脚付き) a low tray table / a hakozen personal tray box
glass 使わない
candle(屋内一般) 行灯。蝋燭は高級品なので提灯と燭台のみ
fork plate(洋皿) chopsticks a shallow ceramic dish
chair bench a low wooden stool(屋台の腰掛けのみ)

不定時法 — 時刻カードは季節で現代時刻が変わる。season と必ず一致させる

呼称 春・秋
明六つ 06:00 04:30 06:40
五つ 08:00 07:00 08:30
昼九つ 12:00 12:00 12:00
八つ 14:00 14:30 13:40
七つ 16:00 17:00 15:00
暮六つ 18:00 19:00 17:10
四つ(夜) 22:00 22:30 21:30

現代との違いバンク(gap の型)

値段の逆転/今は捨てる部位を食べる・今は食べる部位を捨てる/今より単位が小さい(一切れ売り・杯売り)/建前と実態(薬と言い張る)/保存が味付けの理由/一日一回しか作らない/家族で1個を分ける/見た目にだけ金を払う/道具を持たない/身分で食べる物が分かれる

豆知識の作り方

fact は「具体物・呼称・工程・数字・反直感」のどれかを必ず持つ。良い例:

  • 呼称:猪は山くじら、鹿は紅葉、牛は牡丹と呼び換えた
  • 数字:そばが十六文、ゆで卵が二十文
  • 工程:味噌を鍋肌に塗ってから煮る
  • 反直感:居酒屋はもともと惣菜を売る商売だった
  • 単位:酒は杯単位で量り売り

他の時代で使うとき

差し替えるのは4つだけ。① 照明器具の特定語 ② ERA_BAN の禁止列挙 ③ 置換辞書 ④ 時刻・単位の対応表。パイプラインとGATEとビルダーはそのまま使える。


8. GATE(検査スクリプト・全文)

validate.py として保存し python3 validate.py scenes.json ledger.json で実行。exit 0 まで直してループ。

機械で判定できることとできないことは分けてある。GATEが通ったから完成ではない。 内容の濃さは PASS 5 の目視でしか見られない。

GATE 見るもの
G1 台帳カバレッジ(未消化ID・存在しないID参照)
G2 時代考証の禁止語(否定文と器具名特定は除外)
G3 カメラ禁止語・顔で埋める表現・右3分の1の枠契約
G4 手(スマホの手が書かれているか)
G5 貪り食い2回以上・現地人限定・本人実食は最大2回
G6 豆知識と現代差の非空・数字入り3個以上・重複なし
G7 場所がシーン数の半分以上・同じ場所3連続なし
G8 無音テストの非空・セリフ依存でない
G9 文字禁止・時刻カードが編集側
G10 8〜12シーン・各10秒
G11 置き撮り最大2回
G13 水増し(シーン数 ≤ 台帳項目数 × 0.8)
G14 音声安全:audioブロックに漢字ゼロ・JSON全体に《》ゼロ・旧セリフ欄(dialogue_display / dialogue_tts / dialogue_line_for_self_recording / other_character_line)の残存ゼロ
G15 台詞の長さ(1話者の台詞40字以内・目安30字)
G17 カメラ方式・手・LOCKの整合性(自撮り/同行者撮影/置き撮り/背面カメラ)

(表示用セリフのルビ検査と固有名詞辞書の検査は、ビルド時に vlog_build.py の SPEECH CHECK が行う。JSONに表示用セリフが入らなくなったため、GATE側では見られない)

# -*- coding: utf-8 -*-
import sys, json, re, collections

BAN_LEX = [
 (r"\blamps?\b",            "裸のlamp → 行灯/瓦灯/提灯/燭台を特定", r"andon|katou|chochin|shokudai|paper lantern"),
 (r"\bmirrors?\b",          "glass mirror → 磨いた銅の柄鏡",        r"bronze"),
 (r"\bspoons?\b",           "spoon → 竹の匙",                       r"bamboo|wooden ladle"),
 (r"\bforks?\b",            "fork 禁止",                            None),
 (r"\bglass(ware)?\b",      "glass 禁止",                           None),
 (r"\bchairs?\b",           "chair 禁止 → 腰掛け",                  None),
 (r"\blegged (dining )?table\b", "脚付き食卓 禁止 → 箱膳",          None),
 (r"\b(light ?bulb|electric|gas light|matches)\b", "近代設備 禁止", None),
 (r"\b(onion|tomato|cabbage|maize|corn)\b", "江戸に無い野菜",       None),
]
NEG = re.compile(r"\b(no|not|never|without|nor)\b", re.I)

def lex_hits(text):
    """否定文(no glass / not one chair)と、器具名で特定済みの語は違反にしない。"""
    out = []
    for rx, msg, allow in BAN_LEX:
        for m in re.finditer(rx, text, re.I):
            head = text.rfind(".", 0, m.start()); c1 = text.rfind(",", 0, m.start())
            head = max(head, c1, text.rfind(";", 0, m.start()))
            clause = text[head + 1:m.start()]
            if NEG.search(clause): continue
            if allow and re.search(allow, text[max(0, m.start() - 45):m.end() + 15], re.I): continue
            out.append(msg)
    return out

BAN_CAM = [
 (r"\bthe camera pans?\b", "pan 禁止"), (r"\btracking shot\b", "tracking 禁止"),
 (r"\bdolly\b", "dolly 禁止"), (r"\bcrane\b", "crane 禁止"),
 (r"\bdrone\b", "drone 禁止"), (r"\bstatic wide\b", "static wide 禁止"),
 (r"follows her from behind", "三人称 禁止"),
 (r"her face fills the frame", "顔で埋めるの禁止"),
 (r"MONEY SHOT", "MONEY SHOT表記 禁止(顔アップを誘発する)"),
]
FRAME_CONTRACT = "RIGHT EDGE of the vertical frame"
FACT_NUM = re.compile(r"[0-90-9]|[一二三四五六七八九十百千万]||||||||||||||人前")
KANJI = re.compile(r"[一-鿿々]")
OLD_SPEECH_KEYS = ("dialogue_line_for_self_recording", "other_character_line",
                   "dialogue_display", "dialogue_tts")

def check(scenes, ledger, exempt=None):
    exempt = exempt or {}
    err = []
    by_ep = collections.defaultdict(list)
    for s in scenes: by_ep[s["episode_id"]].append(s)

    for ep, S in by_ep.items():
        S = sorted(S, key=lambda x: x["scene_number"])
        tag = lambda m: err.append("[%s] %s" % (ep, m))

        if not (8 <= len(S) <= 12): tag("G10 シーン数が%d(8〜12)" % len(S))
        for s in S:
            if s.get("duration_seconds") != 10: tag("G10 S%s 10秒でない" % s["scene_number"])

        need = {x["id"] for x in ledger if x.get("episode") == ep}
        got = set()
        for s in S:
            c = s.get("covers") or []
            if not c: tag("G1 S%s covers が空" % s["scene_number"])
            got |= set(c)
        miss = sorted(need - got)
        if miss: tag("G1 台帳の未消化: %s" % ", ".join(miss))
        ghost = sorted(got - need)
        if ghost: tag("G1 台帳に無いID参照: %s" % ", ".join(ghost))

        for s in S:
            blob = " ".join(str(s.get(k, "")) for k in
                ("visual_prompt", "set_lock", "lighting", "food_block", "hands", "no_ip"))
            blob_nb = blob.replace(s.get("no_ip", ""), "")
            for msg in set(lex_hits(blob_nb)): tag("G2 S%s %s" % (s["scene_number"], msg))
            cam = json.dumps(s.get("camera", {}), ensure_ascii=False) + " " + str(s.get("visual_prompt", ""))
            for rx, msg in BAN_CAM:
                if re.search(rx, cam, re.I): tag("G3 S%s %s" % (s["scene_number"], msg))
            if s.get("camera_mode") == "front_cam_self" and FRAME_CONTRACT not in str(s.get("visual_prompt", "")):
                tag("G3 S%s 右3分の1の枠契約が入っていない" % s["scene_number"])

        # G17: camera mode, hand ownership and LOCK must agree.
        allowed_modes = {"front_cam_self", "companion_handheld", "propped_static", "rear_cam"}
        modes = [s.get("camera_mode") for s in S]
        for s in S:
            mode = s.get("camera_mode")
            if mode not in allowed_modes:
                tag("G17 S%s 未対応のcamera_mode: %s" % (s.get("scene_number"), mode))
            h = str(s.get("hands") or "")
            vp = str(s.get("visual_prompt") or "")
            beats = " ".join(str(b.get("action", "")) for b in (s.get("beats") or []))
            alltext = " ".join([h, vp, beats])
            if mode in ("front_cam_self", "rear_cam"):
                if "スマホ" not in h and "phone" not in h.lower():
                    tag("G17 S%s 自撮り/背面カメラなのにhandsへスマホが無い" % s["scene_number"])
                if re.search(r"両手|both hands|two hands|with both", alltext, re.I):
                    tag("G17 S%s 自撮り/背面カメラで両手作業を指定" % s["scene_number"])
                if mode == "rear_cam" and re.search(r"face.*frame|selfie|front camera|顔を映", alltext, re.I):
                    tag("G17 S%s rear_camで顔/自撮りを指定" % s["scene_number"])
            elif mode == "companion_handheld":
                if "スマホ" in h or "phone" in h.lower():
                    tag("G17 S%s companion_handheldのhandsにスマホがある" % s["scene_number"])
                if re.search(r"she holds her phone|she is holding her phone|camera IS her phone|客観カメラは存在しない|her own arm shake", alltext, re.I):
                    tag("G17 S%s companion_handheldに自撮りLOCKが残っている" % s["scene_number"])
                if not re.search(r"左手|右手|LEFT hand|RIGHT hand", h, re.I):
                    tag("G17 S%s companion_handheldの左右の手が未指定" % s["scene_number"])
            elif mode == "propped_static":
                if re.search(r"handheld|operator movement|her own arm shake|手持ち|手ぶれ", alltext, re.I):
                    tag("G17 S%s propped_staticに手持ち/操作/手ぶれがある" % s["scene_number"])
                if "スマホ" in h or "phone" in h.lower():
                    tag("G17 S%s propped_staticのhandsにスマホがある" % s["scene_number"])

        companion_count = sum(1 for s in S if s.get("camera_mode") == "companion_handheld")
        if companion_count > 2:
            tag("G17 companion_handheldが%d回(最大2)" % companion_count)

        for s in S:
            h = str(s.get("hands", ""))
            if not h: tag("G4 S%s hands が空" % s["scene_number"])
            elif s.get("camera_mode") in ("front_cam_self", "rear_cam") and ("スマホ" not in h and "phone" not in h.lower()):
                tag("G4 S%s hands にスマホの手が無い" % s["scene_number"])

        dev = [s for s in S if s.get("devour_level") == "devouring"]
        if len(dev) < 2 and ep not in exempt:
            tag("G5 貪り食うシーンが%d(2以上必須。食事が主題でない回は理由付きで例外登録)" % len(dev))
        for s in dev:
            if s.get("who_eats") != "local":
                tag("G5 S%s devouring は現地の人だけ(who_eats=local)" % s["scene_number"])
        nl = [s for s in S if s.get("who_eats") == "noelle"]
        if len(nl) > 2: tag("G5 本人実食が%d回(最大2)" % len(nl))

        facts = []
        for s in S:
            f, g = str(s.get("edo_fact") or ""), str(s.get("modern_gap") or "")
            if not f: tag("G6 S%s edo_fact が空" % s["scene_number"])
            if not g: tag("G6 S%s modern_gap が空" % s["scene_number"])
            facts.append(f)
        strong = sum(1 for f in facts if FACT_NUM.search(f))
        if strong < 3: tag("G6 数字を含む豆知識が%d(3以上必須)" % strong)
        dup = [k for k, v in collections.Counter(facts).items() if k and v > 1]
        if dup: tag("G12 豆知識の重複: %s" % dup)

        cap = int(len(need) * 0.8)
        if need and len(S) > cap:
            tag("G13 台帳%d項目に対しシーンが%d(上限%d)。水増しシーンを削る" % (len(need), len(S), cap))

        locs = [s.get("location_id") for s in S]
        need_loc = (len(S) + 1) // 2
        if len(set(locs)) < need_loc:
            tag("G7 場所が%d箇所(%d以上必須)" % (len(set(locs)), need_loc))
        run = 1
        for i in range(1, len(locs)):
            run = run + 1 if locs[i] == locs[i-1] else 1
            if run > 2: tag("G7 S%s で同じ場所が3連続" % S[i]["scene_number"])

        for s in S:
            sl = str(s.get("silent_test_beat") or "")
            if not sl: tag("G8 S%s 無音テストが空" % s["scene_number"])
            elif re.search(r"言う|説明|セリフ|話す|ナレーション", sl):
                tag("G8 S%s 無音テストがセリフに依存している" % s["scene_number"])

        for s in S:
            if "No text, captions" not in str(s.get("no_ip", "")):
                tag("G9 S%s no_ip に文字禁止が無い" % s["scene_number"])
            tc = s.get("time_card")
            if tc and tc.get("render") != "editor_only_never_generated":
                tag("G9 S%s 時刻カードが生成側になっている" % s["scene_number"])

        for s in S:
            a = s.get("audio", {}) or {}
            ablob = json.dumps(a, ensure_ascii=False)
            if KANJI.search(ablob):
                bad = "".join(sorted(set(KANJI.findall(ablob))))
                tag("G14 S%s audioブロックに漢字[%s]。発話入力はかなのみ" % (s["scene_number"], bad))
            sblob = json.dumps(s, ensure_ascii=False)
            if "《" in sblob or "》" in sblob:
                tag("G14 S%s JSONに《》ルビが残っている(漢字とルビを両方発音される)" % s["scene_number"])
            for old in OLD_SPEECH_KEYS:
                if old in a or old in s:
                    tag("G14 S%s 旧セリフ欄 %s が残っている(発話は speech_script 一箇所だけ)"
                        % (s["scene_number"], old))
            script = a.get("speech_script") or ""
            for line in [x for x in re.split(r"[^)]*", script) if x.strip()]:
                body = re.sub(r"[、。!?!?…・~\s]", "", line)
                if len(body) > 40:
                    tag("G15 S%s 台詞が%d字(上限40・目安30)。発話単位を分けるか削る"
                        % (s["scene_number"], len(body)))

        pr = sum(1 for s in S if s.get("camera_mode") == "propped_static")
        if pr > 2: tag("G11 置き撮りが%d回(最大2)" % pr)
    return err

if __name__ == "__main__":
    scenes = json.load(open(sys.argv[1], encoding="utf-8"))
    ledger = json.load(open(sys.argv[2], encoding="utf-8"))
    exempt = json.load(open(sys.argv[3], encoding="utf-8")) if len(sys.argv) > 3 else {}
    e = check(scenes, ledger, exempt)
    if e:
        print("GATE FAILED: %d件" % len(e))
        for x in e: print(" -", x)
        sys.exit(1)
    print("GATE PASSED")

検査は必ず全話に走らせる。 部分的に走らせて「通りました」と報告しない。


9. ビルダー(全文)

vlog_build.py として保存し python3 vlog_build.py episodes.py 出力名 で実行。xlsx とコピーボタン付きHTMLが出る。

# -*- coding: utf-8 -*-
"""時代Vlog ビルダー:エピソード定義 → シーンJSON → xlsx + コピーボタンHTML
使い方:  python3 vlog_build.py episodes.py 出力名
episodes.py は EPISODES というリストを持つ(構造は SKILL.md 第5章)。"""
import sys, json, html, re, importlib.util, collections

# ======================= LOCK文字列(一字一句変えない) =======================
NOELLE = ("A young woman in modern casual clothing, the only person in modern dress in the entire scene. "
 "She holds her phone in her LEFT hand at arm's length, filming herself; her left forearm is visible at the "
 "bottom-left of the frame; the phone itself is never visible. She stands at the RIGHT EDGE of the vertical "
 "frame occupying about one third of it, and the subject she is showing fills the remaining two thirds on the "
 "LEFT. Her face is never allowed to fill the frame.")
COMPANION = ("A companion films the recurring host in a single continuous handheld shot. The companion stays "
 "behind the recording device and remains outside the image. Both of the host's hands are free and visible. "
 "Her LEFT hand supports the bowl from underneath while her RIGHT hand handles only the bamboo scoop. "
 "Natural handheld movement comes from the unseen companion.")
PROPPED = ("A young woman in modern casual clothing, the only person in modern dress in the entire scene. Her "
 "phone has been propped against a fixed object and films from there; the phone itself is never visible. The "
 "framing is low, off-level and slightly tilted, at the height of the object it rests on. Both of her hands are "
 "free and working.")
REAR = ("The host holds the phone in her LEFT hand with the rear camera facing forward; the phone itself is never "
 "visible. Her face is not in frame and only her RIGHT hand is free. The forward subject fills the frame; no "
 "objective third-person camera or crew exists.")
VLOG_RULE = ("VLOG CAMERA RULE: the camera IS her phone, held in one hand at arm's length. There is no objective "
 "third-person camera and no crew. One hand is always occupied holding the phone, so only ONE hand is free. The "
 "phone is never visible. All reframing is motivated by her physically turning or flipping the phone. She occupies "
 "about one third of the frame at the RIGHT EDGE and never fills it; the subject fills the remaining two thirds on the LEFT.")
COMPANION_RULE = ("VLOG CAMERA RULE: an unseen companion holds the recording device. The companion and device remain "
 "outside the image; the host's both hands are free. Natural handheld movement comes from the companion, not the host. "
 "Do not generate the companion as an Edo character.")
PROPPED_RULE = ("VLOG CAMERA RULE: the phone is placed on a fixed support outside the image. The composition, camera "
 "position, angle and height remain unchanged for the full ten seconds. The host's both hands are free; there is no "
 "operator movement and no handheld shake.")
REAR_RULE = ("VLOG CAMERA RULE: the host holds the phone herself with the rear camera facing forward. Her face is not "
 "shown, only one hand is free, and every framing change is caused by her physical turn or tilt of the phone. No "
 "objective third-person camera or crew exists.")
ANDON = ("Interior light comes only from a wood-framed andon lantern with white paper panels on four sides, a "
 "shallow ceramic dish of rapeseed oil with a twisted wick burning inside it, giving a dim orange glow that barely "
 "reaches the walls. No glass of any kind, no candle, no Western lamp.")
KATOU = ("Interior light comes only from a small fired-clay hooded katou lamp stand with a shallow oil dish under "
 "the hood, the flame partly shielded, reaching only about an arm's length. No glass, no candle.")
CHOCHIN = ("Light comes from collapsible paper lanterns on bamboo frames with candles inside, hung or carried, "
 "the paper stained and patched.")
SHOKUDAI = "Light comes from a tall shokudai candle stand with a thick Japanese wax candle. No glass, no Western lamp."
DEVOUR = ("DEVOURING: he grips the bowl in both hands and clamps it, tips it straight in from the rim without using "
 "chopsticks, his throat working as he swallows without chewing, broth running down his chin which he does not wipe, "
 "his cheek bulging as he pushes more in, and while he is still chewing his other hand is already reaching for the "
 "next one. He leans forward over the bowl with his shoulders hunched around it. Loud audible slurping. He tilts the "
 "bowl completely vertical for the last of it, sets it down hard and stands up immediately.")
DEVOUR_KUSHI = ("DEVOURING: he takes it in both hands and bites straight through it without pausing, jaw working hard, "
 "grease at the corner of his mouth which he does not wipe, and while he is still chewing his other hand is already "
 "holding out the next coin. He eats standing, leaning in over the counter.")
PHY = ("Natural weight, cloth and hair move realistically, handheld shake comes from her own arm, no morphing or sliding")
PHY_COMPANION = ("Natural weight, cloth and hair move realistically, subtle handheld shake comes from the unseen companion, "
 "not from the host's arms, no morphing or sliding")
PHY_PROPPED = ("Natural weight, cloth and hair move realistically, the camera remains physically fixed for the full ten seconds, "
 "no handheld shake, no morphing or sliding")
PHY_REAR = ("Natural weight, cloth and hair move realistically, handheld movement comes from the host's one phone-holding arm, "
 "no morphing or sliding")
SKIN = "Preserve natural skin from the reference - pores, peach fuzz, no smoothing"
COSTUME = ("Present-day casual clothing appropriate to the season; she is the only person in modern dress. "
 "Every other person wears period clothing.")
REF = "@Image 1 - keep face, hair and present-day outfit consistent with the reference sheet"
ERA_BAN = ("Interior night light comes only from andon paper lanterns or hooded clay katou lamps burning rapeseed oil; "
 "street light only from candle-lit paper chochin. No light bulb, no glass lamp chimney, no Western lamp, no gas light. "
 "No glass mirror, no Western spoon or fork, no chairs, no legged dining table - meals are eaten from hakozen personal "
 "tray boxes. No onion, tomato, cabbage or maize. No stainless steel, no clear glass vessels, no matches.")
NO_IP = ("No visible camera, phone, tripod, gimbal or selfie stick. No text, captions, subtitles, numbers or lettering "
 "anywhere in the image; paper lanterns and signboards may carry indistinct ink marks but never readable characters. "
 "No modern clothing except the protagonist - her own present-day clothing is the single intentional anachronism and "
 "every other person is in period dress. No brand logos, no real persons. " + ERA_BAN)

SPEECH_RULE = ("SPEECH RULE: the ONLY text ever spoken aloud in this clip is the audio.speech_script field. It is "
 "written entirely in Japanese kana. Speaker tags in round brackets are silent labels identifying who speaks and are "
 "never pronounced. Read the script exactly once, in order, at a natural pace; do not repeat, restart, or add any "
 "words. No other field of this JSON is speech. Speak Japanese only - no English speech, no translation, no "
 "narration, no on-screen subtitles.")

# ============ 音声安全(EP97の二重読み事故の再発防止。表示用と音声用を分離する) ============
# 固有名詞の発音辞書(全話共通)。複合固有名詞は一語として登録し、ttsには必ずこの読みを一字も変えずに使う。
# 新しい固有名詞は episodes.py の先頭で PRONOUNCE.update({...}) してから使う。勝手な読みで書かない。
PRONOUNCE = {
 "北斎":"ほくさい","葛飾北斎":"かつしかほくさい","お栄":"おえい","応為":"おうい",
 "勝川春章":"かつかわしゅんしょう","勝川":"かつかわ","北斎漫画":"ほくさいまんが",
 "本所割下水":"ほんじょわりげすい","本所":"ほんじょ","吉原":"よしわら","浅草":"あさくさ",
 "江戸":"えど","長崎":"ながさき","日本橋":"にほんばし","両国":"りょうごく",
}
KANA_OK = re.compile(r"^[ぁ-ゖァ-ヺー、。!?…・~  ()]*$")
RUBY_RX = re.compile(r"[一-鿿々]+[^》]*")
def unrubied(text):
    return re.findall(r"[一-鿿々]+", RUBY_RX.sub("", text or ""))

def speech_script(s):
    """JSONに入る唯一の発話テキストを tts / oth_tts だけから組む。表示用(jp/oth)は絶対に使わない。"""
    script = ""
    if s.get("tts"): script = "(ノエル)" + s["tts"]
    if s.get("oth_tts"): script = (script + " " if script else "") + s["oth_tts"]
    return script

def check_speech(ep, s):
    """ビルド時の音声検査。落ちたらビルドごと止める(不正なJSONを1枚も出さないため)。"""
    errs = []
    for name, disp, tts in (("jp/tts", s.get("jp") or "", s.get("tts") or ""),
                            ("oth/oth_tts", s.get("oth") or "", s.get("oth_tts") or "")):
        if disp and not tts:
            errs.append("%s: 表示セリフがあるのに音声用かなが無い。自動変換はせず手で書く" % name)
        if tts and not KANA_OK.match(tts):
            bad = "".join(sorted(set(re.findall(r"[^ぁ-ゖァ-ヺー、。!?…・~  ()]", tts))))
            errs.append("%s: 音声用テキストにかな以外の文字[%s]。数字も固有名詞も全部かなで書く" % (name, bad))
        if disp:
            miss = unrubied(disp)
            if miss:
                errs.append("%s: 表示セリフにルビ無し漢字[%s]" % (name, "".join(sorted(set(miss)))))
            plain = RUBY_RX.sub(lambda m: m.group(0).split("《")[0], disp)
            for k, yomi in PRONOUNCE.items():
                if k in plain and yomi not in tts:
                    errs.append("%s: 固有名詞「%s」の読み「%s」がttsに無い(辞書と一字も違えない)" % (name, k, yomi))
        body = re.sub(r"[^)]*|[、。!?…・~\s]", "", tts)
        if len(body) > 40:
            errs.append("%s: 台詞%d字(上限40・目安30)。長さ超過は反復・再スタート事故の温床" % (name, len(body)))
    if errs:
        raise SystemExit("SPEECH CHECK FAILED %s S%s:\n - %s" % (ep["ep"], s.get("n"), "\n - ".join(errs)))

# ======================= 圧縮と並べ替え =======================
def trim(ep, S, cuts=None, order=None):
    """cuts=削るシーン番号 / order=trim後の並べ替え。どちらも省略可。"""
    out = [s for s in S if s["n"] not in set(cuts or [])]
    out = sorted(out, key=lambda x: x["n"])
    for i, s in enumerate(out, 1): s["n"] = i
    if order:
        assert sorted(order) == list(range(1, len(out)+1)), ep + " のORDERが不正"
        out = [out[k-1] for k in order]
        for i, s in enumerate(out, 1): s["n"] = i
    return out

# ======================= シーン → JSON =======================
def scene_json(ep, s):
    check_speech(ep, s)
    locks = {"front_cam_self": NOELLE, "companion_handheld": COMPANION,
             "propped_static": PROPPED, "rear_cam": REAR}
    rules = {"front_cam_self": VLOG_RULE, "companion_handheld": COMPANION_RULE,
             "propped_static": PROPPED_RULE, "rear_cam": REAR_RULE}
    if s["mode"] not in locks:
        raise ValueError("unsupported camera mode: " + str(s["mode"]))
    lock = locks[s["mode"]]
    cast = " ".join(ep["C"][k] for k in s.get("cast", []))
    vp = ep["L"][s["loc"]] + (" " + cast + "." if cast else "") + " " + lock + " " + s["act"]
    tc = s.get("tc")
    o = collections.OrderedDict()
    o["episode_id"]=ep["ep"]; o["schema_version"]="v8"; o["format"]=ep["fmt"]
    o["era_lock"]=ep.get("era","Bunsei era, Edo, 1818-1830"); o["season_lock"]=ep["season"]
    o["scene_number"]=s["n"]; o["scene_role"]=s["role"]; o["covers"]=s.get("covers",[])
    o["duration_seconds"]=10; o["aspect_ratio"]="9:16"
    o["model_target"]="Higgsfield Seedance 2.5"
    o["generation_settings"]="9:16 / 10s / 720p / Unlimited mode ON"
    o["time_card"]=({"edo":tc[0],"modern":tc[1],"position":"center_upper_then_shrink_topleft",
                     "render":"editor_only_never_generated"} if tc else None)
    o["vlog_rule"]=rules[s["mode"]]; o["camera_mode"]=s["mode"]; o["hands"]=s["hands"]
    o["beats"]=[{"t":t,"action":a} for t,a in zip(["0-3s","3-7s","7-10s"], s["b"])]
    o["visual_prompt"]=vp; o["costume"]=COSTUME; o["character_reference"]=REF
    o["location_id"]=s["loc"]; o["set_lock"]=ep["L"][s["loc"]]
    o["cast_locks"]=[ep["C"][k] for k in s.get("cast",[])]
    o["edo_fact"]=s.get("fact"); o["modern_gap"]=s.get("gap")
    o["who_eats"]=s.get("eats","none"); o["devour_level"]=s.get("dev","none")
    o["prop_thread"]=s.get("prop"); o["silent_test_beat"]=s.get("sil")
    o["camera"]=dict(zip(["angle","movement","shot_type","lens","depth_of_field"], s["cam"].split(" / ")))
    o["lighting"]=s["lit"]; o["food_block"]=s.get("food"); o["motion_intensity"]=s["mi"]
    script = speech_script(s)
    o["audio"]={"ambient":s["amb"],
                "speech_script":script,
                "speech_language":"ja-JP",
                "speech_instruction":SPEECH_RULE,
                "no_speech":(script=="")}
    o["speech_kana_count"]=len(re.sub(r"[^)]*|[、。!?…・~\s]", "", script))
    o["language_lock"]={"visual_instruction_language":"English",
                         "spoken_dialogue_language":"Japanese only","spoken_locale":"ja-JP",
                         "speech_source_field":"audio.speech_script","no_english_speech":True}
    physics = {"front_cam_self": PHY, "companion_handheld": PHY_COMPANION,
               "propped_static": PHY_PROPPED, "rear_cam": PHY_REAR}[s["mode"]]
    o["physics"]=physics; o["skin"]=(None if s["mode"]=="propped_static" else SKIN); o["no_ip"]=NO_IP
    if ep.get("job"): o["job_role"]=ep["job"]
    return json.dumps(o, ensure_ascii=False, indent=2)

# ======================= xlsx =======================
def build_xlsx(EPS, rows, path, title):
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
    F="Arial"; wb=Workbook(); ws=wb.active; ws.title="使い方"
    thin=Side(style="thin",color="D0D0D0"); bd=Border(left=thin,right=thin,top=thin,bottom=thin)
    for r in [[title,""],["",""],["生成前に毎回",""],
      ["Higgsfield","9:16 / 10秒 / Unlimited mode ON"],
      ["時刻カード","編集で載せる。Higgsfieldには生成させない"],
      ["キャラ参照","@Image 1 を毎回つける"],["",""],["見方",""],
      ["オレンジの行","現地の人が貪り食うカット"],
      ["豆知識 / 今との違い","そのシーンで伝える内容。空のシーンは作らない"],
      ["JSON列","そのままコピーしてHiggsfieldへ貼る"]]: ws.append(r)
    ws["A1"].font=Font(name=F,bold=True,size=15)
    for c in ("A3","A8"): ws[c].font=Font(name=F,bold=True,size=12,color="1F3864")
    ws.column_dimensions["A"].width=16; ws.column_dimensions["B"].width=110
    for row in ws.iter_rows():
        for c in row:
            if not c.font.bold: c.font=Font(name=F,size=11)
            c.alignment=Alignment(vertical="top",wrap_text=True)
    heads=["EP","Sc","時刻","場所ID","豆知識","今との違い","食べる人","表示セリフ(ルビ付き・編集用)","音声かな(実際に読まれる)","JSON(これをコピー)"]
    for ep in EPS:
        w=wb.create_sheet(ep["ep"])
        w["A1"]=ep["ep"]+"|"+ep["title"]; w["A1"].font=Font(name=F,bold=True,size=13)
        w.append([]); w.append(heads)
        for c in w[3]:
            c.fill=PatternFill("solid",fgColor="1F3864")
            c.font=Font(name=F,bold=True,color="FFFFFF",size=11)
            c.alignment=Alignment(vertical="center",horizontal="center",wrap_text=True)
        for r in [x for x in rows if x["ep"]==ep["ep"]]:
            who = "現地★貪り" if r["dev"]=="devouring" else ("Noelle" if r["eats"]=="noelle" else "-")
            w.append([r["ep"],r["n"],r["tc"],r["loc"],r["fact"] or "",r["gap"] or "",who,r["jp"],r["script"],r["js"]])
        for row in w.iter_rows(min_row=4):
            for c in row:
                c.font=Font(name=F,size=10); c.border=bd
                c.alignment=Alignment(vertical="top",wrap_text=True)
            if row[6].value and "貪" in str(row[6].value):
                row[6].fill=PatternFill("solid",fgColor="FDE9D9")
            row[9].fill=PatternFill("solid",fgColor="EEF3FA")
            row[9].alignment=Alignment(vertical="top",wrap_text=False)
            w.row_dimensions[row[0].row].height=64
        for col,wd in zip("ABCDEFGHIJ",[7,5,13,17,28,30,11,30,30,120]): w.column_dimensions[col].width=wd
        w.freeze_panes="A4"
    wb.save(path); return path

# ======================= コピーボタンHTML =======================
CSS = """body{font-family:-apple-system,"Hiragino Sans","Yu Gothic",sans-serif;max-width:920px;margin:0 auto;padding:24px;color:#1a1a1a;background:#fff}
h1{font-size:22px;font-weight:500}h2{font-size:18px;font-weight:500;margin-top:40px;border-bottom:1px solid #ddd;padding-bottom:8px;scroll-margin-top:60px}
.cnt{font-size:12px;color:#888;font-weight:400}
.bok{font-size:11.5px;background:#e6f2e6;color:#2c6e2c;border-radius:5px;padding:2px 7px;font-weight:400}
.bng{font-size:11.5px;background:#f7e3e3;color:#9b2c2c;border-radius:5px;padding:2px 7px;font-weight:400}
.note{background:#eef3fa;border-radius:10px;padding:12px 16px;font-size:13px;line-height:1.8}
.nav{position:sticky;top:0;background:#fff;border-bottom:1px solid #e5e5e5;padding:10px 0;margin:16px 0 0;font-size:12.5px;z-index:5}
.nav a{display:inline-block;margin:2px 6px 2px 0;color:#1f3864;text-decoration:none;border:1px solid #d5dced;border-radius:5px;padding:2px 7px}
.nav a:hover{background:#eef3fa}
.card{border:1px solid #e3e3e3;border-radius:12px;padding:14px 16px;margin:12px 0}
.hd{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
.pill{background:#1f3864;color:#fff;font-size:12px;padding:3px 10px;border-radius:6px}
.ttl{font-weight:500;font-size:15px}.meta{font-size:12px;color:#888}
.dev{background:#c0501e;color:#fff;font-size:11.5px;padding:3px 9px;border-radius:6px}
.tas{background:#e8e8e8;color:#444;font-size:11.5px;padding:3px 9px;border-radius:6px}
.fact{background:#fdf6ec;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.7;margin-bottom:8px}
pre{background:#fafafa;border-radius:8px;padding:10px 12px;max-height:190px;overflow:auto;font-size:11.5px;line-height:1.5;white-space:pre-wrap;margin:0}
.row{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
button{font:inherit;font-size:13px;padding:7px 14px;border:1px solid #999;background:#fff;border-radius:6px;cursor:pointer}
button:hover{background:#f2f2f2}.jp{font-size:12.5px;color:#666}"""
JS = """function cp(t,b){const o=b.textContent;const d=()=>{b.textContent='コピーしました';setTimeout(()=>b.textContent=o,1400)};
if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(t).then(d).catch(()=>fb(t,d))}else fb(t,d)}
function fb(t,d){const x=document.createElement('textarea');x.value=t;x.style.cssText='position:fixed;left:-9999px';document.body.appendChild(x);x.select();try{document.execCommand('copy');d()}catch(e){}document.body.removeChild(x)}
document.querySelectorAll('.cp').forEach(b=>b.addEventListener('click',e=>cp(e.target.closest('.card').querySelector('pre').textContent,e.target)));
document.querySelectorAll('.all').forEach(b=>b.addEventListener('click',e=>{const s=e.target.closest('section');
cp('['+[...s.querySelectorAll('pre')].map(p=>p.textContent).join(',\\n')+']',e.target)}));"""

def build_html(EPS, rows, path, title, exempt=None):
    exempt = exempt or {}
    nav=" ".join('<a href="#%s">%s</a>'%(e["ep"],e["ep"]) for e in EPS)
    secs=[]
    for ep in EPS:
        R=[x for x in rows if x["ep"]==ep["ep"]]
        nd=sum(1 for r in R if r["dev"]=="devouring"); nf=sum(1 for r in R if r["fact"])
        ex=exempt.get(ep["ep"]); dev_ok = nd>=2 or bool(ex)
        badge=('<span class="cnt">%dシーン</span> <span class="%s">%s</span> <span class="%s">%s</span>'%(
            len(R),"bok" if dev_ok else "bng",("貪り%d(対象外)"%nd) if ex else ("貪り%d"%nd),
            "bok" if nf==len(R) else "bng","豆知識あり" if nf==len(R) else "豆知識%d/%d"%(nf,len(R))))
        cards=[]
        for r in R:
            tag='<span class="dev">現地の人が貪り食う</span>' if r["dev"]=="devouring" else (
                '<span class="tas">本人が味わう</span>' if r["eats"]=="noelle" else "")
            tc='<span class="meta">%s</span>'%html.escape(r["tc"]) if r["tc"] else ""
            fx=('<div class="fact"><b>豆知識</b> %s<br><b>今との違い</b> %s</div>'
                %(html.escape(r["fact"] or ""),html.escape(r["gap"] or ""))) if r["fact"] else ""
            cards.append('<div class="card"><div class="hd"><span class="pill">S%d</span>%s'
              '<span class="ttl">%s</span>%s</div>%s<pre>%s</pre>'
              '<div class="row"><button class="cp">JSONをコピー</button><span class="jp">%s</span></div></div>'
              %(r["n"],tc,html.escape(r["role"]),tag,fx,html.escape(r["js"]),
                html.escape((r["jp"] or "")+((" |音声: "+r["script"]) if r["script"] else ""))))
        secs.append('<section id="%s"><h2>%s%s %s</h2>'
          '<button class="all">この話を全部コピー</button>%s</section>'
          %(ep["ep"],ep["ep"],html.escape(ep["title"]),badge,"".join(cards)))
    page=('<!DOCTYPE html><html lang="ja"><meta charset="utf-8"><title>%s</title>\n<style>%s</style>\n'
          '<h1>%s</h1>\n<div class="note">生成前に毎回:<b>9:16</b> / <b>10秒</b> / <b>Unlimited mode ON</b>。'
          '時刻カードは編集で載せます。<b>オレンジのカードが「現地の人が貪り食う」シーン</b>です。'
          '見出しのバッジはデータから自動計算しています。</div>\n<div class="nav">%s</div>\n%s'
          '<script>%s</script></html>')%(title,CSS,title,nav,"".join(secs),JS)
    open(path,"w",encoding="utf-8").write(page); return path

# ======================= 実行 =======================
def run(EPS, out_xlsx, out_html, title, cuts=None, order=None, exempt=None):
    cuts=cuts or {}; order=order or {}
    rows=[]
    for ep in EPS:
        ep["S"]=trim(ep["ep"], ep["S"], cuts.get(ep["ep"]), order.get(ep["ep"]))
        for s in ep["S"]:
            tc=s.get("tc")
            rows.append(dict(ep=ep["ep"],n=s["n"],loc=s["loc"],role=s["role"],
                tc=(tc[0]+" / "+tc[1]) if tc else "", fact=s.get("fact"), gap=s.get("gap"),
                eats=s.get("eats","none"), dev=s.get("dev","none"), jp=s.get("jp",""),
                script=speech_script(s), js=scene_json(ep,s)))
    print("xlsx", build_xlsx(EPS,rows,out_xlsx,title))
    print("html", build_html(EPS,rows,out_html,title,exempt))
    return rows

if __name__ == "__main__":
    spec=importlib.util.spec_from_file_location("eps", sys.argv[1])
    m=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
    name=sys.argv[2] if len(sys.argv)>2 else "vlog"
    run(m.EPISODES, name+".xlsx", name+".html", getattr(m,"TITLE",name),
        getattr(m,"CUTS",None), getattr(m,"ORDER",None), getattr(m,"DEVOUR_EXEMPT",None))

検査用のJSONを吐くには scene_json の結果を json.loads して配列にする。

import json
from vlog_build import run
rows = run(EPISODES, "out.xlsx", "out.html", TITLE, CUTS, ORDER, DEVOUR_EXEMPT)
json.dump([json.loads(r["js"]) for r in rows], open("scenes.json","w",encoding="utf-8"), ensure_ascii=False)

10. キャラクター参照シート

  • 主人公の顔・髪・現代服は 参照画像(@Image 1)で固定する。 テキストで外見を長く描写しない(参照と喧嘩する)
  • 衣装は参照シートから引き継がない。 エピソードごとに季節に合った現代の普段着を指定する
  • 現代服なのは主人公ひとりだけ。 他は全員その時代の服装。これが唯一の意図的な時代違反であることを NO_IP に明記してある
  • 参照シートを作るときも、上の原則どおり顔ではなく服・髪型・持ち物で固定する

11. 既知の失敗モードと対応(毎回ここを読む)

# 起きたこと 根本原因 防ぐもの
1 参考素材の3品中2品が消えた 台帳が無くテーマから書いた PASS 0 台帳 + G1
2 動画の核(製造工程)が一行に潰れた 工程を1シーンに圧縮した 台帳で1手順1IDに分解
3 視点を間違えた(店主の一日→客視点) 型判定をしなかった PASS 2 型判定4軸
4 洋燈・ガラス鏡・西洋スプーンが出る 英語の一般名詞を書いた G2 + 置換辞書
5 顔がアップで料理と人が消えた 「MONEY SHOT・顔で埋める」テンプレ G3 + 右3分の1の枠契約
6 映画っぽくなった(三人称カメラ) pan/tracking を書いた G3 カメラ禁止語
7 手が3本になった 自由な手の管理をしていなかった G4 hands 必須
8 実食が全部本人だった 主役=本人という思い込み G5 貪り2回以上・現地人限定
9 豆知識が薄い/消えた シーンごとの必須項目でなかった G6 + PASS 5 の目視
10 同じ場所が3連続 ROUTE表を作らなかった PASS 2 + G7
11 音を切ると何も分からない セリフで説明していた G8 無音テスト
12 画面に生成文字が出る 時刻カードを生成側に書いた G9 editor_only
13 古いファイルが残って混乱 作り直すたびに新ファイルを作った PASS 6 1範囲1組
14 コピーしにくい平文になった 読みやすさを優先した 出力契約:JSONのまま
15 内容が薄いのに12シーンあった シーン数を先に固定した PASS 1 + G13
16 一部の話だけふりがなが無かった 途中から始めた慣習を遡らなかった G14 + 原則4
17 同じ範囲のファイルが何組もできた 作り直すたびに新しい名前で出した PASS 6 20話ごとに統合
18 セットログ型が途中で終わった 締めのシーンを尺の都合で削った PASS 2:起床で始めたら就寝で閉じる
19 圧縮で貪り食いのシーンを削った 削除後に GATE を掛け直さなかった 削ったら必ず PASS 4 をやり直す
20 「全話に入っています」と書いたが入っていなかった 出力を数えずに断定した 原則5。HTMLの見出しバッジをデータから自動計算する
21 漢字とルビを両方発音(身体→「しんたいからだ」) ルビ付きセリフをJSONに入れていた 原則6:表示と音声の分離 + G14(audioに漢字ゼロ・《》ゼロ)
22 「おぼえるえてほん」のような壊れた読み かなを機械で自動変換した ttsは手書きの原則 + ビルダーのSPEECH CHECK
23 「ほんじょわりげすい」→「ほんしょわりがすい」 複合固有名詞をモデルが分割した PRONOUNCE辞書 + PASS 8 の音声検査
24 同じ台詞を途中から再スタート・反復 セリフ欄が複数並存し、長さ超過も重なった speech_script一本化 + G15長さ上限 + PASS 8

12. 会話の進め方

  • 着手前に AskUserQuestion で ①題材(参考素材の有無)②型の希望 ③本数 を確認する
  • 台帳を作ったら必ずユーザーに見せて確認を取る
  • GATE のログは隠さず出す。何件落ちて何を直したかを短く報告する
  • PASS 5 の逆照合表は毎回必ず出す。「入れました」だけの報告はしない
  • 一度に作るのは最大3話。10話たまったら PASS 7
  • 例外(貪り食いを2つ置けない回など)は理由を書いて登録する。黙って基準を下げない

13. 公開まわり

  • 概要欄にAI生成である旨を明記し、YouTubeの「合成コンテンツ」ラベルをONにする
  • 参考素材は事実・工程・盛り付けの参照であり、文章と映像をそのまま複製しない
  • Instagramリール用ハッシュタグは 上5個が本命、下段が保険の二段構成

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/noellesugar99-higgsfield-runner-vlogall/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

noellesugar99-higgsfield-runner-vlogall.ocm.jsonjson
{
  "ocm": "1",
  "id": "noellesugar99-higgsfield-runner-vlogall",
  "kind": "skill",
  "name": "vlogall",
  "description": "Complete self-contained pipeline for producing time-travel history vlog episodes as scene-by-scene JSON for Higgsfield Seedance (8-12 scenes x 10s, 9:16, vertical selfie vlog), including the source-video analysis prompt, ledger format, full episode authoring format with a complete worked example, the Japanese dialogue voice guide, the period-accuracy pack, and the full source code of both the validation gate and the xlsx/copy-button-HTML builder. Use for any request to create, rebuild, audit, fact-check, compress, or export episodes of an Edo or other historical vlog channel. Everything needed to reproduce the output is inside this skill - no external files required.",
  "publisher": "noellesugar99",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding",
      "math"
    ],
    "tags": [
      "skill-md",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Complete self-contained pipeline for producing time-travel history vlog episodes as scene-by-scene JSON for Higgsfield Seedance (8-12 scenes x 10s, 9:16, vertical selfie vlog), including the source-video analysis prompt, ledger format, full episode authoring format with a complete worked example, the Japanese dialogue voice guide, the period-accuracy pack, and the full source code of both the validation gate and the xlsx/copy-button-HTML builder. Use for any request to create, rebuild, audit, fact-check, compress, or export episodes of an Edo or other historical vlog channel. Everything needed to reproduce the output is inside this skill - no external files required."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/noellesugar99/higgsfield-runner",
      "path": "extras/vlogall/SKILL.md",
      "ref": "9ec9e683e4d1cfd3e255cc601a389426c445f0a9",
      "url": "https://github.com/noellesugar99/higgsfield-runner/blob/9ec9e683e4d1cfd3e255cc601a389426c445f0a9/extras/vlogall/SKILL.md",
      "key": "noellesugar99/higgsfield-runner/extras/vlogall/SKILL.md"
    }
  },
  "instructions": "# 時代タイムトラベルVlog 完全版(vlogall)\n\n参考動画の内容を、一人称Vlogのシーン別JSONに変換し、Higgsfield Seedanceへそのまま貼れる形(コピーボタン付きHTML+xlsx)で出す。\n\n**このskillは単体で完結している。** 材料の作り方・書式・見本・文体・考証・検査スクリプト・ビルダーのコード、全部この中にある。外部ファイルは要らない。\n\n---\n\n## 0. 設計原則\n\n**過去の失敗は全部「ルールを守り忘れた」ではなく「守れたかどうかを誰も判定できなかった」ことが原因だった。**\n\n> **原則1:正解を持つのはモデルの記憶ではなく、台帳(LEDGER)と検査スクリプト。**\n> **原則2:検査が通っていない出力は、絶対にユーザーへ見せない。**\n> **原則3:文章で書いても守れないルールは、機械が判定できる形に変換してから書き始める。**\n> **原則4:ルールを1つ増やしたら、必ず既存の全話へ遡って掛け直す。**\n> **原則5:自分の出力について「必ず入っています」と書く前に、その場でデータを数える。** 書いた記憶と、入っている事実は別物。\n> **原則6:音声として読まれてよいテキストは、JSONの中に一箇所だけ・かな書きだけで置く。** 表示用(ルビ付き)と音声用を同じJSONに同居させない。EP97で、ルビ付きセ",
  "cost": {
    "context_tokens": 12921
  }
}

Fetch it by URL: GET /api/v1/registry/noellesugar99-higgsfield-runner-vlogall/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.