@wji.ai/cli 0.9.1 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +109 -32
  3. package/package.json +1 -1
  4. package/skill/backreel/SKILL.md +29 -15
  5. package/skill/backreel/reference/crowdfunding.md +61 -30
  6. package/skill/backreel/reference/episodes.md +7 -7
  7. package/skill/short-drama/SKILL.md +27 -4
  8. package/skill/short-drama/examples/night-shift/episodes/E01.json +173 -1
  9. package/skill/short-drama/examples/night-shift/project.json +6 -0
  10. package/skill/short-drama/reference/dialogue-craft.md +16 -11
  11. package/skill/short-drama/reference/prompt-audit.mjs +308 -0
  12. package/skill/short-drama/reference/reference-binding.md +84 -0
  13. package/skill/short-drama/reference/schema.md +14 -5
  14. package/skill/short-drama/reference/stage-characters.md +1 -1
  15. package/skill/short-drama/reference/stage-script.md +54 -7
  16. package/skill/short-drama/reference/stage-storyboard.md +29 -24
  17. package/skill/short-drama/scripts/selftest.mjs +154 -9
  18. package/skill/short-drama/scripts/short-drama.mjs +467 -23
  19. package/skill/w-film-dialogue-editor/CHANGELOG.md +44 -0
  20. package/skill/w-film-dialogue-editor/SKILL.md +124 -0
  21. package/skill/w-film-dialogue-editor/references/data/README.md +222 -0
  22. package/skill/w-film-dialogue-editor/references/data/en/core.json +1666 -0
  23. package/skill/w-film-dialogue-editor/references/data/en/slang.json +1602 -0
  24. package/skill/w-film-dialogue-editor/references/data/en/work.json +1722 -0
  25. package/skill/w-film-dialogue-editor/references/data/en-sentences.csv +134 -0
  26. package/skill/w-film-dialogue-editor/references/data/en-sentences.md +146 -0
  27. package/skill/w-film-dialogue-editor/references/data/ja/core.json +1978 -0
  28. package/skill/w-film-dialogue-editor/references/data/ja/slang.json +1927 -0
  29. package/skill/w-film-dialogue-editor/references/data/ja/work.json +1972 -0
  30. package/skill/w-film-dialogue-editor/references/data/ja-sentences.csv +134 -0
  31. package/skill/w-film-dialogue-editor/references/data/ja-sentences.md +146 -0
  32. package/skill/w-film-dialogue-editor/references/data/ko/core.json +1839 -0
  33. package/skill/w-film-dialogue-editor/references/data/ko/slang.json +1897 -0
  34. package/skill/w-film-dialogue-editor/references/data/ko/work.json +1829 -0
  35. package/skill/w-film-dialogue-editor/references/data/ko-sentences.csv +134 -0
  36. package/skill/w-film-dialogue-editor/references/data/ko-sentences.md +146 -0
  37. package/skill/w-film-dialogue-editor/references/data/zh-cn/core.json +1794 -0
  38. package/skill/w-film-dialogue-editor/references/data/zh-cn/slang.json +1609 -0
  39. package/skill/w-film-dialogue-editor/references/data/zh-cn/work.json +1648 -0
  40. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.csv +134 -0
  41. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.md +146 -0
  42. package/skill/w-film-dialogue-editor/references/data/zh-hk/core.json +2476 -0
  43. package/skill/w-film-dialogue-editor/references/data/zh-hk/gen-z.json +3263 -0
  44. package/skill/w-film-dialogue-editor/references/data/zh-hk/older.json +3632 -0
  45. package/skill/w-film-dialogue-editor/references/data/zh-hk/rough.json +1668 -0
  46. package/skill/w-film-dialogue-editor/references/data/zh-hk/workplace.json +3697 -0
  47. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.csv +373 -0
  48. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.md +391 -0
  49. package/skill/w-film-dialogue-editor/references/data/zh-tw/core.json +1640 -0
  50. package/skill/w-film-dialogue-editor/references/data/zh-tw/slang.json +1537 -0
  51. package/skill/w-film-dialogue-editor/references/data/zh-tw/work.json +1699 -0
  52. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.csv +132 -0
  53. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.md +144 -0
  54. package/skill/w-film-dialogue-editor/references/dialogue_revision_framework.md +84 -0
  55. package/skill/w-film-dialogue-editor/references/en-index.md +265 -0
  56. package/skill/w-film-dialogue-editor/references/ja-index.md +633 -0
  57. package/skill/w-film-dialogue-editor/references/ko-index.md +498 -0
  58. package/skill/w-film-dialogue-editor/references/lang-en.md +242 -0
  59. package/skill/w-film-dialogue-editor/references/lang-ja.md +312 -0
  60. package/skill/w-film-dialogue-editor/references/lang-ko.md +290 -0
  61. package/skill/w-film-dialogue-editor/references/lang-zh-cn.md +60 -0
  62. package/skill/w-film-dialogue-editor/references/lang-zh-hk-mandarin.md +34 -0
  63. package/skill/w-film-dialogue-editor/references/lang-zh-hk.md +306 -0
  64. package/skill/w-film-dialogue-editor/references/lang-zh-period.md +38 -0
  65. package/skill/w-film-dialogue-editor/references/lang-zh-tw.md +57 -0
  66. package/skill/w-film-dialogue-editor/references/zh-cn-index.md +636 -0
  67. package/skill/w-film-dialogue-editor/references/zh-hk-index.md +1450 -0
  68. package/skill/w-film-dialogue-editor/references/zh-tw-index.md +457 -0
  69. package/skill/w-film-dialogue-editor/scripts/rebuild.sh +13 -0
  70. package/skill/w-film-dialogue-editor/scripts/ref.mjs +539 -0
  71. package/skill/w-film-dialogue-editor/templates/dialogue_revision_report.md +43 -0
package/README.md CHANGED
@@ -311,10 +311,15 @@ generations, banner sizes, AI-disclosure compliance); `social-media` holds
311
311
  every platform's image / video specs (Instagram, TikTok, YouTube, Facebook, X,
312
312
  LinkedIn, Pinterest, Threads, Snapchat, 小红书, 抖音, 快手, 视频号, B站, 微博,
313
313
  公众号) and the repurposing recipes — an episode into 9:16 teasers and posters,
314
- an ad master into every placement. Install them the same way:
314
+ an ad master into every placement. `skill/w-film-dialogue-editor/` is the
315
+ dialogue skill short-drama reviews every script with: a guide per 语体 (粤语,
316
+ 大陆普通话, 台湾国语, 港式普通话, 古装/年代, English, 韩文, 日文) and a corpus of
317
+ real usage queried with its `scripts/ref.mjs`; short-drama finds it when it is
318
+ installed beside it. Install them the same way:
315
319
 
316
320
  ```bash
317
321
  cp -r "$(npm root -g)/@wji.ai/cli/skill/short-drama" ~/.claude/skills/short-drama
322
+ cp -r "$(npm root -g)/@wji.ai/cli/skill/w-film-dialogue-editor" ~/.claude/skills/w-film-dialogue-editor
318
323
  cp -r "$(npm root -g)/@wji.ai/cli/skill/ads" ~/.claude/skills/ads
319
324
  cp -r "$(npm root -g)/@wji.ai/cli/skill/social-media" ~/.claude/skills/social-media
320
325
  ```
package/dist/index.js CHANGED
@@ -6349,7 +6349,7 @@ async function uploadToVod(token, file, onProgress) {
6349
6349
  // src/commands/backreel-episode.ts
6350
6350
  var MAX_EPISODES = 30;
6351
6351
  var VIDEO_EXTS = [".mp4", ".mov", ".m4v", ".webm"];
6352
- var ASSET_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
6352
+ var VIDEO_MAX_BYTES = 500 * 1024 * 1024;
6353
6353
  var subtitlesOf = (e) => (e.subtitles || []).map((s) => ({ id: s.id, lang: s.lang, ...s.label ? { label: s.label } : {} }));
6354
6354
  function toDto(e, order) {
6355
6355
  const base = { title: e.title, order, tier: e.tier || "public", subtitles: subtitlesOf(e) };
@@ -6376,8 +6376,8 @@ async function resolveSource(client, opts, postId, file) {
6376
6376
  const ext = extname2(file).toLowerCase();
6377
6377
  if (!VIDEO_EXTS.includes(ext)) throw new Error(`${basename6(file)}: episodes must be ${VIDEO_EXTS.join(" / ")}`);
6378
6378
  const size = (await stat5(file)).size;
6379
+ if (size > VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 500 MB`);
6379
6380
  if (opts.legacy) {
6380
- if (size > ASSET_VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 200 MB \u2014 drop --legacy to upload it to VOD`);
6381
6381
  info(color.dim(`Uploading ${basename6(file)} as a WJI asset\u2026`));
6382
6382
  const asset = await client.upload("/assets/v2/upload", file, {});
6383
6383
  return { video: asset.id };
@@ -6452,7 +6452,7 @@ function pickRendition(renditions, want) {
6452
6452
  }
6453
6453
  return hit;
6454
6454
  }
6455
- var sourceOptions = (cmd) => cmd.option("--vid <vid>", "a Vid already in the Backreel VOD space").option("--asset <assetId>", "an uploaded WJI video asset (legacy path, public URL)").option("--legacy", "upload the file as a WJI asset (\u2264200 MB) instead of VOD");
6455
+ var sourceOptions = (cmd) => cmd.option("--vid <vid>", "a Vid already in the Backreel VOD space").option("--asset <assetId>", "an uploaded WJI video asset (legacy path, public URL)").option("--legacy", "upload the file as a WJI asset (\u2264500 MB) instead of VOD");
6456
6456
  function episodeCommand(globals2) {
6457
6457
  const ep = new Command19("episode").aliases(["episodes", "ep"]).description("Episodes of a showcase post: upload, reorder, gate by tier, check transcoding");
6458
6458
  ep.command("list <postId>", { isDefault: true }).description("List the episodes with their tier, source and VOD status").action(async (postId) => {
@@ -6955,7 +6955,11 @@ function enquiryCommand(globals2) {
6955
6955
 
6956
6956
  // src/commands/backreel-fund.ts
6957
6957
  import { Command as Command22 } from "commander";
6958
- var CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
6958
+ var CASH_CURRENCIES = ["hkd", "usd", "twd", "sgd", "eur", "gbp"];
6959
+ var CURRENCIES = ["credits", ...CASH_CURRENCIES];
6960
+ var MODES = ["credits", "cash", "both"];
6961
+ var hasCredits = (mode) => mode === "credits" || mode === "both";
6962
+ var fmt = (n) => Math.round(Number(n) || 0).toLocaleString("en-US");
6959
6963
  var MAX_TIERS = 10;
6960
6964
  var num = (value, flag, min = 0) => {
6961
6965
  const n = Number(value);
@@ -6975,6 +6979,10 @@ function itemIndex(n, count, what) {
6975
6979
  }
6976
6980
  function campaignDto(c) {
6977
6981
  return {
6982
+ // fundingMode / currency / creditGoal: the server keeps the stored value when a field is left out
6983
+ fundingMode: c?.fundingMode ?? void 0,
6984
+ currency: c?.fundingMode && c.fundingMode !== "credits" ? c?.currency ?? void 0 : void 0,
6985
+ creditGoal: c?.creditGoal ?? void 0,
6978
6986
  goalAmount: c?.goalAmount ?? 0,
6979
6987
  offlineRaisedAmount: c?.offlineRaisedAmount ?? 0,
6980
6988
  deadline: c?.deadline ?? null,
@@ -7004,22 +7012,40 @@ var put = (client, id, body) => client.put(`${POSTS}/list/${id}`, body);
7004
7012
  function printFund(post) {
7005
7013
  const c = post.campaign;
7006
7014
  const d = post.donation || {};
7007
- const cur = (d.currency || c?.currency || "hkd").toUpperCase();
7015
+ const mode = c?.fundingMode || "cash";
7016
+ const cur = (c?.currency || d.currency !== "credits" && d.currency || "hkd").toUpperCase();
7008
7017
  if (c) {
7009
- const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
7010
- detail([
7011
- ["goal", `${c.goalAmount} ${cur}`],
7012
- ["raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`],
7013
- ["supporters", c.supporterCount],
7014
- ["deadline", c.deadline || color.dim("none")]
7015
- ]);
7018
+ const rows = [["funding", mode]];
7019
+ if (hasCredits(mode)) {
7020
+ const pct = c.creditGoal ? ` (${Math.round((c.creditsRaised || 0) / c.creditGoal * 100)}%)` : "";
7021
+ rows.push(
7022
+ ["credits goal", `${fmt(c.creditGoal)} credits`],
7023
+ ["credits raised", `${fmt(c.creditsRaised)} credits${pct} from ${c.creditBackerCount ?? 0} backer(s)`],
7024
+ ["outcome", c.outcome === "funded" ? color.green("funded") : c.outcome === "failed" ? color.red("failed \u2014 refunded") : "raising"]
7025
+ );
7026
+ }
7027
+ if (mode !== "credits") {
7028
+ const pct = c.goalAmount ? ` (${Math.round((c.raisedAmount || 0) / c.goalAmount * 100)}%)` : "";
7029
+ rows.push(
7030
+ ["cash goal", `${c.goalAmount} ${cur}`],
7031
+ ["cash raised", `${c.raisedAmount} ${cur}${pct} = offline ${c.offlineRaisedAmount ?? 0} + online ${(c.raisedAmount || 0) - (c.offlineRaisedAmount || 0)}`]
7032
+ );
7033
+ }
7034
+ if (c.production) {
7035
+ rows.push([
7036
+ "production",
7037
+ `${c.production.status} \u2014 ${c.production.milestonesApproved}/${c.production.milestonesTotal} milestones (contract: \`wji br contract get ${c.production.contractJobId}\`)`
7038
+ ]);
7039
+ }
7040
+ rows.push(["supporters", c.supporterCount], ["deadline", c.deadline || color.dim("none")]);
7041
+ detail(rows);
7016
7042
  process.stdout.write("\n");
7017
7043
  } else {
7018
7044
  info(color.dim(`${post.kind} post \u2014 no campaign (goal / offline supporters are crowdfunding-only); donations still work`));
7019
7045
  }
7020
7046
  detail([
7021
7047
  ["donations", d.enabled ? color.green("on") : "off"],
7022
- ["currency", (d.currency || "hkd").toUpperCase()],
7048
+ ["currency", d.currency === "credits" ? "WJI credits" : (d.currency || "hkd").toUpperCase()],
7023
7049
  ["minimum", d.minAmount ?? 0]
7024
7050
  ]);
7025
7051
  if ((d.tiers || []).length) {
@@ -7048,13 +7074,17 @@ function fundCommand(globals2) {
7048
7074
  const post = await loadPost(client, postId);
7049
7075
  render({ campaign: post.campaign, donation: post.donation, kind: post.kind }, () => printFund(post));
7050
7076
  });
7051
- fund.command("set <postId>").description("Campaign goal / offline total / deadline and donation switches; only what you pass changes").option("--goal <amount>", "campaign goal (\u22651 to publish a crowdfunding post)").option("--offline-raised <amount>", "money raised outside Backreel, added to the progress bar").option("--deadline <date>", 'ISO date (e.g. 2026-12-31); "none" clears \u2014 donations stop after it').option("--donations <on|off>", "accept online donations (Stripe)").option("--currency <code>", `${CURRENCIES.join(" | ")} \u2014 locked once a donation exists`).option("--min <amount>", "minimum donation").action(async (postId, opts) => {
7077
+ fund.command("set <postId>").description("Funding type, goals, deadline and donation switches; only what you pass changes").option("--mode <mode>", `${MODES.join(" | ")} \u2014 credits: any creator, all-or-nothing at the deadline; cash / both need Cash fundraiser verification`).option("--credit-goal <credits>", "credits goal (credits / both campaigns)").option("--goal <amount>", "cash goal (cash / both campaigns)").option("--cash-currency <code>", `${CASH_CURRENCIES.join(" | ")} \u2014 currency of the cash goal`).option("--offline-raised <amount>", "money raised outside Backreel, added to the progress bar").option("--deadline <date>", 'ISO date (e.g. 2026-12-31); "none" clears \u2014 donations stop after it').option("--donations <on|off>", "accept support (credits pledges, or legacy Stripe donations)").option("--currency <code>", `${CURRENCIES.join(" | ")} \u2014 credits = WJI credits; can't leave credits once someone pledged`).option("--min <amount>", "minimum donation").action(async (postId, opts) => {
7052
7078
  const client = backreelClient(globals2);
7053
7079
  const post = await loadPost(client, postId);
7054
7080
  const body = {};
7055
- if (opts.goal !== void 0 || opts.offlineRaised !== void 0 || opts.deadline !== void 0) {
7081
+ const campaignFlags = [opts.goal, opts.offlineRaised, opts.deadline, opts.mode, opts.creditGoal, opts.cashCurrency];
7082
+ if (campaignFlags.some((v) => v !== void 0)) {
7056
7083
  requireCampaign(post);
7057
7084
  const campaign = campaignDto(post.campaign);
7085
+ if (opts.mode !== void 0) campaign.fundingMode = oneOf2(String(opts.mode).toLowerCase(), MODES, "--mode");
7086
+ if (opts.creditGoal !== void 0) campaign.creditGoal = Math.round(num(opts.creditGoal, "--credit-goal", 1));
7087
+ if (opts.cashCurrency !== void 0) campaign.currency = oneOf2(String(opts.cashCurrency).toLowerCase(), CASH_CURRENCIES, "--cash-currency");
7058
7088
  if (opts.goal !== void 0) campaign.goalAmount = num(opts.goal, "--goal");
7059
7089
  if (opts.offlineRaised !== void 0) campaign.offlineRaisedAmount = num(opts.offlineRaised, "--offline-raised");
7060
7090
  if (opts.deadline !== void 0) {
@@ -7066,9 +7096,12 @@ function fundCommand(globals2) {
7066
7096
  }
7067
7097
  }
7068
7098
  body.campaign = campaign;
7099
+ if (hasCredits(campaign.fundingMode) && opts.currency === void 0 && opts.donations === void 0) {
7100
+ body.donation = { ...donationDto(post.donation), enabled: true, currency: "credits" };
7101
+ }
7069
7102
  }
7070
7103
  if (opts.donations !== void 0 || opts.currency !== void 0 || opts.min !== void 0) {
7071
- const donation = donationDto(post.donation);
7104
+ const donation = body.donation || donationDto(post.donation);
7072
7105
  if (opts.donations !== void 0) donation.enabled = oneOf2(String(opts.donations).toLowerCase(), ["on", "off"], "--donations") === "on";
7073
7106
  if (opts.currency !== void 0) donation.currency = oneOf2(String(opts.currency).toLowerCase(), CURRENCIES, "--currency");
7074
7107
  if (opts.min !== void 0) donation.minAmount = Math.round(num(opts.min, "--min"));
@@ -7185,6 +7218,46 @@ function fundCommand(globals2) {
7185
7218
  if ((r.payouts || []).length) info(color.dim(`${r.payouts.length} payout(s) \u2014 payouts and refunds are handled by Backreel staff`));
7186
7219
  });
7187
7220
  });
7221
+ fund.command("pledges <postId>").description("Credits backers of your post (anonymous backers stay hidden)").option("--limit <n>", "how many (\u2264200)", "100").action(async (postId, opts) => {
7222
+ const client = backreelClient(globals2);
7223
+ const r = await client.get(`/backreel/pledge/post/${postId}/manage`, { limit: Number(opts.limit) });
7224
+ render(r, () => {
7225
+ detail([
7226
+ ["credits", fmt(r.stats?.credits ?? 0)],
7227
+ ["backers", r.stats?.backerCount ?? 0],
7228
+ ["pledges", r.stats?.pledgeCount ?? 0]
7229
+ ]);
7230
+ process.stdout.write("\n");
7231
+ table(r.data || [], [
7232
+ { header: "when", value: (p) => relativeTime(p.createdAt) },
7233
+ { header: "backer", value: (p) => p.isAnonymous ? color.dim("anonymous") : truncate(p.backer?.name, 20) },
7234
+ { header: "credits", value: (p) => fmt(p.credits) },
7235
+ { header: "tier", value: (p) => truncate(p.tier?.title, 16) },
7236
+ { header: "status", value: (p) => p.status },
7237
+ { header: "message", value: (p) => truncate(p.message, 30) }
7238
+ ]);
7239
+ });
7240
+ });
7241
+ fund.command("back <postId> <credits>").description("Back someone's post with your WJI credits \u2014 campaigns hold them until the deadline (refunded if the goal is missed); showcase works send them to the creator").option("--tier <n>", "pick tier n (see `fund show`)").option("-m, --message <text>", "a note to the creator (\u2264500)").option("--anonymous", "hide your name on the supporter wall").option("-y, --yes", "skip confirmation").action(async (postId, credits, opts) => {
7242
+ const client = backreelClient(globals2);
7243
+ const post = await client.get(`${POSTS}/list/${postId}`);
7244
+ if (post.donation?.currency !== "credits" || !post.donation?.enabled) {
7245
+ throw new Error(`"${post.title}" doesn't take credits support`);
7246
+ }
7247
+ const tiers = post.donation.tiers || [];
7248
+ const tier2 = opts.tier ? tiers[itemIndex(opts.tier, tiers.length, "Tier")] : null;
7249
+ const amount = Math.round(num(credits, "<credits>", 1));
7250
+ const where = post.kind === "crowdfunding" ? "held by the campaign until its deadline" : "sent to the creator";
7251
+ if (!await confirm(`Back "${post.title}" with ${fmt(amount)} credits (${where})?`, opts.yes)) return info("Aborted");
7252
+ const pledge = await client.post("/backreel/pledge", {
7253
+ post: postId,
7254
+ credits: amount,
7255
+ tierId: tier2?.id,
7256
+ message: opts.message,
7257
+ isAnonymous: !!opts.anonymous
7258
+ });
7259
+ render(pledge, () => success(`Backed "${post.title}" with ${fmt(pledge.credits)} credits \u2014 ${pledge.status}`));
7260
+ });
7188
7261
  return fund;
7189
7262
  }
7190
7263
 
@@ -7211,12 +7284,12 @@ function parseDay(value, flag) {
7211
7284
  return d.toISOString();
7212
7285
  }
7213
7286
  var usd = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7214
- var fmt = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7287
+ var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7215
7288
  function budgetText(j) {
7216
7289
  if (j.budgetNegotiable && j.budgetMin == null && j.budgetMax == null) return "negotiable";
7217
- const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt(n)}`;
7290
+ const cur = j.currency === "usd" || !j.currency ? usd : (n) => `${String(j.currency).toUpperCase()} ${fmt2(n)}`;
7218
7291
  const cash = j.budgetMin != null && j.budgetMax != null && j.budgetMin !== j.budgetMax ? `${cur(j.budgetMin)}\u2013${cur(j.budgetMax)}` : j.budgetMax != null || j.budgetMin != null ? cur(j.budgetMax ?? j.budgetMin) : "";
7219
- return [cash, j.budgetCredits ? `${fmt(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
7292
+ return [cash, j.budgetCredits ? `${fmt2(j.budgetCredits)} credits` : ""].filter(Boolean).join(" + ");
7220
7293
  }
7221
7294
  function jobOptions2(cmd) {
7222
7295
  return cmd.option("--category <c>", `one of ${CATEGORIES2.join(", ")}`).option("--brief <text|@file>", "what you need (max 5000)").option("--deliverables <text|@file>", "what gets delivered (max 2000)").option("--budget <min[-max]>", 'cash budget in --currency, e.g. 8000 or 5000-10000; "none" clears').option("--currency <code>", "currency of --budget (converted to USD by the server)", "USD").option("--negotiable", "budget is negotiable").option("--credits <n>", "WJI credits budget on top of cash; 0 clears").option("--duration <len>", "running time: 90, 3m, 1h30m, 2:30").option("--resolution <r>", `delivery resolution: ${RESOLUTIONS.join(", ")}`).option("--upscale", "upscaling to the resolution is acceptable").option("--no-upscale", "must be generated at the resolution").option("--model <name...>", "required WJI models (max 10)").option("--deadline <date>", "bid deadline, YYYY-MM-DD (\u2264 90 days)").option("--delivery <date>", 'delivery date, YYYY-MM-DD; "none" clears').option("--location <text>", "location, if any").option("--link <url...>", "reference links (max 5)").option("--verified-only", "only verified creators can bid").option("--open-to-all", "anyone can bid (undo --verified-only)").option("--org <orgId>", 'post as one of your organizations; "none" = personal');
@@ -7294,8 +7367,8 @@ function printBids(rows) {
7294
7367
  { header: "job", value: (b) => truncate(b.job?.title, 24) },
7295
7368
  { header: "bidder", value: (b) => truncate(b.organization?.name || b.bidder?.name, 18) },
7296
7369
  { header: "status", value: (b) => b.status },
7297
- { header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt(b.price)}` },
7298
- { header: "credits", value: (b) => fmt(b.creditsPrice) },
7370
+ { header: "price", value: (b) => b.price == null ? "" : b.currency === "usd" || !b.currency ? usd(b.price) : `${b.currency.toUpperCase()} ${fmt2(b.price)}` },
7371
+ { header: "credits", value: (b) => fmt2(b.creditsPrice) },
7299
7372
  { header: "days", value: (b) => b.timelineDays ?? "" },
7300
7373
  { header: "pitch", value: (b) => truncate(b.pitch, 30) }
7301
7374
  ]);
@@ -7463,7 +7536,7 @@ var KIND_NAMES = {
7463
7536
  draft: "Draft"
7464
7537
  };
7465
7538
  var usd2 = (n) => n == null ? "" : `US$${Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
7466
- var fmt2 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7539
+ var fmt3 = (n) => n == null ? "" : Math.round(Number(n)).toLocaleString("en-US");
7467
7540
  var msName = (m, i) => m.title || KIND_NAMES[m.kind] || `Payment ${i + 1}`;
7468
7541
  async function loadContract(client, ref) {
7469
7542
  try {
@@ -7506,12 +7579,12 @@ function printContract(c) {
7506
7579
  ["you are", c.mySide],
7507
7580
  ["poster", c.posterOrg?.name ? `${c.posterOrg.name} (org)` : c.poster?.name],
7508
7581
  ["creator", c.creatorOrg?.name ? `${c.creatorOrg.name} (org)` : c.creator?.name],
7509
- ["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt2(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
7582
+ ["total", `${usd2(c.totalUsd)} = cash ${usd2(c.cashUsd)} + ${fmt3(c.credits)} credits (${Math.round((c.creditsShare ?? 0) * 100)}% credits, min ${Math.round((c.minCreditsShare ?? 0.25) * 100)}%)`],
7510
7583
  ["credits release", c.creditsRelease === "milestones" ? "with each payment" : "all at once"],
7511
7584
  ["plan", c.status === "planning" ? `v${c.planVersion}${c.proposedBy ? ` proposed by ${c.proposedBy}` : ""}` : void 0],
7512
7585
  [
7513
7586
  "project credits",
7514
- e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt2(e.settledCreator)} to creator, ${fmt2(e.settledPoster)} to poster` : `${fmt2(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt2(e.topupReturned)} top-up returned` : ""}` : `${fmt2(Math.max(0, e.left))} left of ${fmt2(e.budget ?? e.funded)} (used ${fmt2(e.spent)}${e.toppedUp ? `, creator topped up ${fmt2(e.toppedUp)}` : ""}; overdraft ${fmt2(e.overdraft)})` : void 0
7587
+ e ? e.settledAt ? `settled: ${e.settledTo === "split" ? `${fmt3(e.settledCreator)} to creator, ${fmt3(e.settledPoster)} to poster` : `${fmt3(e.settledAmount)} to ${e.settledTo}`}${e.topupReturned ? `, ${fmt3(e.topupReturned)} top-up returned` : ""}` : `${fmt3(Math.max(0, e.left))} left of ${fmt3(e.budget ?? e.funded)} (used ${fmt3(e.spent)}${e.toppedUp ? `, creator topped up ${fmt3(e.toppedUp)}` : ""}; overdraft ${fmt3(e.overdraft)})` : void 0
7515
7588
  ],
7516
7589
  ["workspace", e?.workspaceId],
7517
7590
  ["cancel request", c.cancelRequest ? `${c.cancelRequest.side}${c.cancelRequest.reason ? `: ${c.cancelRequest.reason}` : ""}` : void 0],
@@ -7524,7 +7597,7 @@ function printContract(c) {
7524
7597
  { header: "payment", value: (m) => truncate(msName(m, c.milestones.indexOf(m)), 22) },
7525
7598
  { header: "due", value: (m) => new Date(m.dueDate).toLocaleDateString() },
7526
7599
  { header: "cash", value: (m) => usd2(m.cashUsd) },
7527
- ...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt2(m.credits) }] : [],
7600
+ ...c.creditsRelease === "milestones" ? [{ header: "credits", value: (m) => fmt3(m.credits) }] : [],
7528
7601
  { header: "status", value: (m) => m.status },
7529
7602
  { header: "revisions", value: (m) => m.revisionsUsed ? `${m.revisionsUsed}/${c.revisionAllowance}` : "" },
7530
7603
  {
@@ -7535,14 +7608,14 @@ function printContract(c) {
7535
7608
  const pending = (c.changeRequests || []).find((r) => r.status === "pending");
7536
7609
  if (pending) {
7537
7610
  info("");
7538
- info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt2(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
7611
+ info(`${color.yellow("Change request waiting")} ${pending.id}: +${fmt3(pending.credits)} credits${pending.cashUsd ? ` + ${usd2(pending.cashUsd)}` : ""} (${pending.reason}) \u2014 ${truncate(pending.note, 60)}`);
7539
7612
  }
7540
7613
  const d = c.dispute;
7541
7614
  if (d && d.status !== "withdrawn") {
7542
7615
  info("");
7543
7616
  info(`${d.status === "open" ? color.yellow("Dispute open") : "Dispute decided"} \u2014 raised by ${d.side}: ${truncate(d.reason, 80)}`);
7544
7617
  for (const st of d.statements || []) info(color.dim(` ${st.side} \xB7 ${relativeTime(st.at)}: ${truncate(st.text, 80)}`));
7545
- if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt2(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
7618
+ if (d.resolution) info(` Decision: ${d.resolution.outcome}${d.resolution.outcome === "cancel" ? ` (${fmt3(d.resolution.creatorCredits)} credits to creator)` : ""} \u2014 ${truncate(d.resolution.note, 60)}`);
7546
7619
  }
7547
7620
  }
7548
7621
  function parsePayment(spec, perMilestone) {
@@ -7645,9 +7718,9 @@ function contractCommand(globals2) {
7645
7718
  const client = backreelClient(globals2);
7646
7719
  const c = await loadContract(client, ref);
7647
7720
  if (c.myCredits != null && c.myCredits < c.credits) {
7648
- throw new Error(`You have ${fmt2(c.myCredits)} general credits; this contract needs ${fmt2(c.credits)}. Top up on WJI first.`);
7721
+ throw new Error(`You have ${fmt3(c.myCredits)} general credits; this contract needs ${fmt3(c.credits)}. Top up on WJI first.`);
7649
7722
  }
7650
- if (!await confirm(`Add ${fmt2(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
7723
+ if (!await confirm(`Add ${fmt3(c.credits)} credits to "${c.job?.title}" and start production?`, opts.yes)) return info("Aborted");
7651
7724
  await run(c.id, "/fund", void 0, (u) => `Funded \u2014 project workspace ${u.escrow?.workspaceId}`);
7652
7725
  });
7653
7726
  ct.command("submit <contract> <payment>").description("Creator: submit a payment (milestone) for review").option("--note <text|@file>", "what you are delivering").option("--link <url...>", "links to the delivery (max 5)").action(async (ref, n, opts) => {
@@ -7695,7 +7768,7 @@ function contractCommand(globals2) {
7695
7768
  ct.command("topup <contract> <credits>").description("Creator: add your own credits to the project (unused ones come back to you)").action(async (ref, credits) => {
7696
7769
  const n = Math.floor(Number(credits));
7697
7770
  if (!(n > 0)) throw new Error("credits must be a positive number");
7698
- await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt2(n)} \u2014 ${fmt2(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
7771
+ await run(ref, "/topup", { credits: n }, (u) => `Topped up ${fmt3(n)} \u2014 ${fmt3(Math.max(0, u.escrow?.left ?? 0))} project credits left`);
7699
7772
  });
7700
7773
  const change = ct.command("change").description("Change requests: ask for more credits / cash when scope changes or extra revisions");
7701
7774
  change.command("request <contract>", { isDefault: true }).description("Creator: propose extra credits and/or cash").option("--credits <n>", "extra credits", "0").option("--cash <n>", "extra cash in USD").option("--reason <r>", `${CR_REASONS.join(", ")}`, "scope").option("--payment <n>", "the payment it relates to (for extra revisions)").requiredOption("--note <text|@file>", "what changed").action(async (ref, opts) => {
@@ -7848,6 +7921,7 @@ function backreelCommand(globals2) {
7848
7921
  { header: "publish", value: (p2) => p2.publishStatus },
7849
7922
  { header: "project", value: (p2) => p2.projectStatus },
7850
7923
  { header: "credits", value: (p2) => (p2.credits || []).length },
7924
+ { header: "interested", value: (p2) => p2.interestCount ?? 0 },
7851
7925
  { header: "updated", value: (p2) => relativeTime(p2.updatedAt) }
7852
7926
  ]);
7853
7927
  const p = r?.pagination;
@@ -7992,7 +8066,6 @@ function backreelCommand(globals2) {
7992
8066
  if (!(p.credits || []).length) advice.push("no credits");
7993
8067
  const pending = episodes.filter((e) => e.vodVid && e.vodStatus !== "ready");
7994
8068
  if (pending.length) advice.push(`${pending.length} episode(s) not ready on VOD (${pending.map((e) => `#${(e.order ?? 0) + 1} ${e.vodStatus || "?"}`).join(", ")}) \u2014 \`backreel episode status\``);
7995
- if (p.trailerDurationSec && p.trailerDurationSec > 31) advice.push(`trailer is ${p.trailerDurationSec}s; the limit is 30s`);
7996
8069
  if (p.donation?.enabled && p.kind === "crowdfunding" && p.campaign?.deadline && new Date(p.campaign.deadline) < /* @__PURE__ */ new Date()) {
7997
8070
  advice.push("the campaign deadline has passed \u2014 donations are closed");
7998
8071
  }
@@ -8070,7 +8143,7 @@ function backreelCommand(globals2) {
8070
8143
  return br;
8071
8144
  }
8072
8145
  function postOptions(cmd) {
8073
- return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video asset (\u226430 s)").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8146
+ return cmd.option("--kind <kind>", KINDS.join(" | ")).option("--logline <text>", `one-line hook, max ${LOGLINE_MAX} chars; public even when the description is locked ("" clears it)`).option("--category <category>", "category").option("--project-status <status>", PROJECT_STATUSES.join(" | ")).option("--tags <tags>", "comma-separated tags (replaces the list)").option("-d, --description <text>", "description, plain text or HTML (@file / - supported)").option("--cover <assetId>", "landscape cover asset").option("--cover-portrait <assetId>", "portrait cover asset").option("--trailer <assetId>", "trailer video asset (\u2264500 MB, any length)").option("--media <assetId...>", `LookBook gallery, images or videos in order (replaces it; \u2264${MEDIA_MAX}; "none" clears)`).option("--org <orgId|none>", "publish as one of your organizations; none = yourself (only the author can change it later)").option("--on-slate <on|off>", "crowdfunding only: also list it on the Slate, not just the Crowdfunding page (default off)").option("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
8074
8147
  }
8075
8148
  async function postBody(opts) {
8076
8149
  const body = { ...await readJson(opts.body) || {} };
@@ -8082,6 +8155,7 @@ async function postBody(opts) {
8082
8155
  }
8083
8156
  if (opts.category) body.category = opts.category;
8084
8157
  if (opts.org !== void 0) body.organization = opts.org === "none" ? null : opts.org;
8158
+ if (opts.onSlate !== void 0) body.showOnSlate = oneOf2(opts.onSlate, ["on", "off"], "--on-slate") === "on";
8085
8159
  if (opts.projectStatus) body.projectStatus = oneOf2(opts.projectStatus, PROJECT_STATUSES, "--project-status");
8086
8160
  if (opts.tags !== void 0) body.tags = String(opts.tags).split(",").map((t) => t.trim()).filter(Boolean);
8087
8161
  if (opts.description !== void 0) body.description = toHtml(await readValue(opts.description));
@@ -8124,10 +8198,13 @@ function printPost(p) {
8124
8198
  ["media", (p.assets || []).length ? `${p.assets.length} item(s)` : void 0],
8125
8199
  ["episodes", (p.episodes || []).length ? `${p.episodes.length} (\`backreel episode list\`)` : void 0],
8126
8200
  ["campaign", p.campaign ? `${p.campaign.raisedAmount}/${p.campaign.goalAmount} ${(p.campaign.currency || "").toUpperCase()}` : void 0],
8201
+ ["deadline", p.campaign?.deadline ? String(p.campaign.deadline).slice(0, 10) : void 0],
8202
+ ["on slate", p.kind === "crowdfunding" ? p.showOnSlate ? "yes" : "no (Crowdfunding page only)" : void 0],
8127
8203
  ["donations", p.donation?.enabled ? "on" : void 0],
8128
8204
  ["trailer", p.trailer ? `${assetLabel(p.trailer)}${p.trailerDurationSec ? ` (${p.trailerDurationSec}s)` : ""}` : void 0],
8129
8205
  ["creator", p.creator?.name],
8130
8206
  ["likes", p.likeCount],
8207
+ ["interested", p.interestCount ? `${p.interestCount} (members get new-episode and campaign-result notices)` : void 0],
8131
8208
  ["updated", relativeTime(p.updatedAt)]
8132
8209
  ]);
8133
8210
  process.stdout.write("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wji.ai/cli",
3
- "version": "0.9.1",
3
+ "version": "0.9.4",
4
4
  "description": "Command line interface for the WJI AI platform",
5
5
  "license": "UNLICENSED",
6
6
  "author": "WJI AI",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: backreel
3
- description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign with donation tiers and offline supporters, read donation records, answer enquiries (查詢 / 合作邀請 / 投資 / 授權) — reply, accept or decline, grant partner access — write blog posts (文章 / 作品動態) on a profile or under a project, and work the Job Plaza (接單廣場) — post a job, place a sealed bid, award it, then run the contract: payment plan, project credits, milestones, top-ups, change requests and disputes. Use whenever a task mentions Backreel, a Backreel post / work / reel, its episodes, trailer, cover or poster sizes, crowdfunding / 眾籌 / 捐款, supporters, enquiries / 查詢 on a creator's work, a Backreel blog post, project update or behind-the-scenes note, or a Backreel job, bid, quote, contract / 合約, milestone or dispute.
3
+ description: Run a creator's works on Backreel (the film / short-drama showcase and crowdfunding site that shares accounts with WJI) with the `wji` CLI — create and publish a post (作品), prepare and upload cover / portrait / LookBook images at the right size and ratio, add cast and crew credits, upload and maintain episode videos (分集 via BytePlus VOD), set who can watch each episode, run a crowdfunding campaign in WJI credits (all-or-nothing, tiers) or, for verified fundraisers, cash with offline supporters, back a post with credits, read backers and donation records, answer enquiries (查詢 / 合作邀請 / 投資 / 授權) — reply, accept or decline, grant partner access — write blog posts (文章 / 作品動態) on a profile or under a project, and work the Job Plaza (接單廣場) — post a job, place a sealed bid, award it, then run the contract: payment plan, project credits, milestones, top-ups, change requests and disputes. Use whenever a task mentions Backreel, a Backreel post / work / reel, its episodes, trailer, cover or poster sizes, crowdfunding / 眾籌 / 捐款, supporters, enquiries / 查詢 on a creator's work, a Backreel blog post, project update or behind-the-scenes note, or a Backreel job, bid, quote, contract / 合約, milestone or dispute.
4
4
  ---
5
5
 
6
6
  # Backreel
@@ -26,11 +26,12 @@ wji br list # your posts, drafts included
26
26
  | kind | `showcase` (episodes, trailer) or `crowdfunding` (goal, supporters) — switching wipes the other side's data | `br create --kind`, `br set --kind` |
27
27
  | title / logline / description | logline = one public line ≤200 chars; description = HTML or plain text | `br set --logline`, `-d @file.md` |
28
28
  | images | 16:9 cover, 9:16 portrait, LookBook gallery (≤9), credit photos | `br image <file> --slot … --post` |
29
- | trailer | a video asset ≤30 s | `wji asset upload` → `br set --trailer` |
29
+ | trailer | a video asset, ≤500 MB, any length | `wji asset upload` → `br set --trailer` |
30
30
  | episodes | ≤30, each with its own viewer tier, hosted on VOD | `br episode …` |
31
31
  | credits | cast / crew / creative, ≤30, may link a WJI user or an NPC | `br credit …` |
32
- | campaign + donations | goal, offline total, deadline, donation tiers, offline supporters | `br fund …` |
32
+ | campaign + support | funding type (credits / cash / both), credits goal, cash goal, deadline, tiers, offline supporters | `br fund …` |
33
33
  | enquiries | messages from people who want to work with / invest in / license the work | `br enquiry …` |
34
+ | where it lists | showcase → the Slate (`/slate`); crowdfunding → the Crowdfunding page (`/crowdfunding`, filterable by Raising / Ended), and also the Slate when the creator opts in | `br set --on-slate on` |
34
35
 
35
36
  Viewer tiers, lowest to highest: `public` (anyone) < `member` (signed in) <
36
37
  `supporter` (has donated) < `partner` (granted by the creator or a verified
@@ -111,24 +112,37 @@ transcoding". Numbers on the command line are 1-based positions. Read
111
112
  [reference/episodes.md](reference/episodes.md) for the trailer, the legacy
112
113
  asset path, tier strategy, subtitles and failure handling.
113
114
 
114
- ## 4. Crowdfunding and donations
115
+ ## 4. Crowdfunding and credits support
115
116
 
116
117
  ```bash
117
- wji br create "下一部長片" --kind crowdfunding -d @pitch.md
118
- wji br fund set <postId> --goal 500000 --deadline 2026-12-31 \
119
- --donations on --currency hkd --min 50
120
- wji br fund tier add <postId> --title "早鳥" --amount 200 --perks "片尾鳴謝" --limit 300
118
+ wji br create "下一部長片" --kind crowdfunding -d @pitch.md # new campaigns default to credits
119
+ wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
120
+ wji br fund tier add <postId> --title "早鳥" --amount 2000 --perks "片尾鳴謝" --limit 300
121
+ wji br fund show <postId> # funding type, progress, outcome, tiers
122
+ wji br fund pledges <postId> # credits backers
123
+ wji br fund back <postId> 500 --tier 1 -y # back someone else's post with your credits
124
+
125
+ # cash — only for publishers with Cash fundraiser verification
126
+ wji br fund set <postId> --mode both --goal 500000 --cash-currency hkd
121
127
  wji br fund supporter add <postId> --name "ABC Films" --amount 50000
122
128
  wji br fund set <postId> --offline-raised 50000 # money raised outside Backreel
123
- wji br fund show <postId> # progress, settings, tiers
124
- wji br fund donations <postId> # online donations, balance, payouts
125
129
  ```
126
130
 
127
- Progress = offline total + online donations. Donations (Stripe) work on any
128
- published post, not just crowdfunding, and stop at the deadline; the currency
129
- locks after the first donation. Payouts and refunds are done by Backreel
130
- staff, not the CLI. Read [reference/crowdfunding.md](reference/crowdfunding.md)
131
- for tier design and what supporters unlock.
131
+ Support is in **WJI credits**, from the backer's wallet — no card. On a
132
+ crowdfunding post the credits are held until the deadline: **all or
133
+ nothing** — goal missed → every backer refunded; goal reached → the campaign
134
+ is `funded` and becomes a production contract released by milestone. On a
135
+ showcase post credits go straight to the creator. Funding type: `credits`
136
+ (any creator), `cash` or `both` (need Cash fundraiser verification for the
137
+ person or organization publishing). Read
138
+ [reference/crowdfunding.md](reference/crowdfunding.md) for the rules and tier
139
+ design.
140
+
141
+ Members can also mark any published work **Interested** (有興趣). It is a
142
+ public count (`interested` in `backreel list` / `backreel get`); only the
143
+ creator sees who, on the site's Manage → Supporters tab. Interested members get
144
+ an in-app notice when a new episode goes live or a credits campaign ends. A
145
+ creator cannot mark their own work, and the CLI cannot set it.
132
146
 
133
147
  ## 5. Enquiries
134
148
 
@@ -1,35 +1,65 @@
1
- # Crowdfunding, donations and supporters
1
+ # Crowdfunding, credits support and supporters
2
2
 
3
- ## Two pieces that work together
3
+ ## Funding types
4
4
 
5
- | | Campaign | Donations |
5
+ | | Credits | Cash |
6
6
  | --- | --- | --- |
7
- | on | crowdfunding posts only | any published post |
8
- | holds | goal, offline total, deadline, offline supporter list | on/off, currency, minimum, tiers |
9
- | set with | `br fund set --goal --offline-raised --deadline`, `br fund supporter …` | `br fund set --donations --currency --min`, `br fund tier …` |
7
+ | who can run it | any creator | publishers (person or organization) with **Cash fundraiser** verification |
8
+ | goal | `--credit-goal` (credits) | `--goal` + `--cash-currency` |
9
+ | how backers give | pledge WJI credits on the site (`br fund back`) — held until the deadline | commitments arranged with Backreel, money paid outside the site, recorded as `--offline-raised` / `fund supporter` |
10
+ | at the deadline | **all or nothing**: goal reached → `funded` (becomes a production contract, credits released by milestone); missed → `failed`, every backer refunded automatically | nothing automatic |
10
11
 
11
- The public progress bar = `--offline-raised` + online donations; the
12
- supporter count = offline supporters + online donors.
12
+ `--mode both` runs both at once with two progress bars. New campaigns default
13
+ to credits. Posts made before funding types existed read as `cash`.
13
14
 
14
- ## Setting up a campaign
15
+ Credits support is also how showcase works take support: credits go straight
16
+ to the creator (no deadline, nothing held). Legacy Stripe donations stay in
17
+ `fund donations` but new support is credits.
18
+
19
+ ## Setting up a credits campaign
15
20
 
16
21
  ```bash
17
22
  wji br create "下一部長片" --kind crowdfunding -d @pitch.md --logline "…"
18
- wji br fund set <postId> --goal 500000 --deadline 2026-12-31
19
- wji br fund set <postId> --donations on --currency hkd --min 50
20
- wji br fund tier add <postId> --title "早鳥支持" --amount 200 --perks "片尾鳴謝 + 數碼海報" --limit 300
21
- wji br fund tier add <postId> --title "製片人" --amount 20000 --perks "聯合製片人名銜、探班一次" --limit 5
23
+ wji br fund set <postId> --mode credits --credit-goal 2000000 --deadline 2026-12-31
24
+ wji br fund tier add <postId> --title "早鳥支持" --amount 2000 --perks "片尾鳴謝 + 數碼海報" --limit 300
25
+ wji br fund tier add <postId> --title "製片人" --amount 200000 --perks "聯合製片人名銜、探班一次" --limit 5
22
26
  wji br check <postId>
23
27
  wji br publish <postId> # ask first
24
28
  ```
25
29
 
26
- - A crowdfunding post cannot publish with a goal below 1.
27
- - Currencies: hkd, usd, twd, sgd, eur, gbp. **Locked after the first
28
- donation** — choose before publishing.
30
+ - `--mode credits` (or `both`) switches credits support on by itself.
31
+ - A credits campaign needs a goal ≥ 1 and a **deadline in the future** to
32
+ publish; it settles when the deadline passes (checked every 10 minutes).
33
+ - **Funded → production contract.** The moment a credits campaign is funded,
34
+ Backreel turns it into a job contract (`wji br contract get <jobId>`, the
35
+ id is in `br fund show`): credits only, released milestone by milestone,
36
+ milestones must add up to exactly the credits raised. The creator sends the
37
+ plan (`br contract plan`), a Backreel admin accepts it on the backers'
38
+ behalf, the project workspace opens with the first milestone's credits, and
39
+ each later milestone unlocks when Backreel approves the previous one. If
40
+ the contract is cancelled, unused credits go back to the backers pro rata;
41
+ if it completes, what's left is the creator's.
42
+ - Once anyone has pledged, the campaign can't drop credits, the post can't be
43
+ deleted or switched to showcase, and support can't go back to cash. After it
44
+ settles the credits goal is fixed.
45
+ - Backers can't take a pledge back; Backreel staff can refund one.
46
+ - Where it lists: every published crowdfunding post is on the Crowdfunding
47
+ page (`/crowdfunding`, filterable by Raising / Ended). It is **not** on the
48
+ Slate unless the creator opts in: `wji br set <postId> --on-slate on`.
29
49
  - Amounts are whole numbers. Every tier must be ≥ the minimum (and ≥ 1);
30
50
  ≤10 tiers; `--limit` caps the slots (`--limit 0` on `tier set` = unlimited).
31
- - The deadline stops donations at that moment; `--deadline none` removes it.
32
- - A single donation is capped at 100,000.
51
+
52
+ ## Cash campaigns (verified fundraisers only)
53
+
54
+ ```bash
55
+ wji br fund set <postId> --mode cash --goal 500000 --cash-currency hkd --deadline 2026-12-31
56
+ wji br fund supporter add <postId> --name "ABC Films" --amount 50000
57
+ wji br fund set <postId> --offline-raised 50000
58
+ ```
59
+
60
+ Without Cash fundraiser verification the server refuses `cash` / `both`;
61
+ apply on the Backreel profile (Settings → verification). A post that already
62
+ had cash keeps it even if the verification later lapses.
33
63
 
34
64
  ## Designing tiers
35
65
 
@@ -58,25 +88,26 @@ Adding a supporter does **not** change the raised amount — update
58
88
  `--offline-raised` yourself (keep the two consistent). Avatars are square
59
89
  images (`br image logo.png --slot photo`).
60
90
 
61
- ## Donation records
91
+ ## Backers and donation records
62
92
 
63
93
  ```bash
64
- wji br fund donations <postId>
94
+ wji br fund pledges <postId> # credits backers: time, backer, credits, tier, status, message
95
+ wji br fund donations <postId> # legacy Stripe donations, balance, payouts
65
96
  ```
66
97
 
67
- Shows raised / paid out / balance, then each donation: time, donor (anonymous
68
- donors stay hidden even from the creator), amount, tier, status (`paid`,
69
- `partially_refunded`, `refunded`, `failed`, `cancelled`; a red *disputed*
70
- marks a card chargeback) and the donor's message. Payouts and
71
- refunds are handled by Backreel staff — the creator asks them, the CLI cannot
72
- do it.
98
+ Pledge status: `held` (campaign running, or funded and waiting for its
99
+ contract), `released` (a showcase tip, already the creator's), `refunded`,
100
+ `transferred` (moved into the production contract). Anonymous backers stay
101
+ hidden even from the creator. Legacy donations: refunds and payouts are
102
+ handled by Backreel staff — the CLI cannot do it.
73
103
 
74
- ## Showcase posts can take donations too
104
+ ## Showcase posts take credits support too
75
105
 
76
- A showcase post (episodes) can turn donations on to sell `supporter` access
77
- to later episodes — there is just no goal or progress bar:
106
+ A showcase post (episodes) can take credits support to sell `supporter`
107
+ access to later episodes — there is no goal or progress bar, and the credits
108
+ go straight to the creator:
78
109
 
79
110
  ```bash
80
- wji br fund set <postId> --donations on --currency hkd --min 30
111
+ wji br fund set <postId> --donations on --currency credits --min 100
81
112
  wji br episode tiers <postId> --public-to 2 --rest supporter
82
113
  ```
@@ -6,19 +6,20 @@
6
6
  | --- | --- | --- |
7
7
  | posts | any | **showcase only** (switching to crowdfunding drops them) |
8
8
  | count | one | ≤30 |
9
- | length | ≤30 s (server allows +1 s) | no limit |
9
+ | length | no limit | no limit |
10
+ | size | ≤500 MB | ≤500 MB |
10
11
  | storage | WJI asset (public URL) | BytePlus VOD (signed URL per play, ~5 min) |
11
12
  | gated | no — always public | per episode: public / member / supporter / partner |
12
13
 
13
14
  ### Trailer
14
15
 
15
16
  ```bash
16
- wji asset upload trailer.mp4 --json # ≤200 MB; duration is probed
17
+ wji asset upload trailer.mp4 --json # ≤500 MB, any length
17
18
  wji br set <postId> --trailer <assetId>
18
19
  ```
19
20
 
20
- The server rejects a trailer over 31 s when it knows the duration. Cut it to
21
- ≤30 s first.
21
+ There is no duration cap. The upload is rejected only when the file is over
22
+ 500 MB.
22
23
 
23
24
  ## Uploading episodes
24
25
 
@@ -33,8 +34,7 @@ transcoding), then saves the episode list with the new `vodVid`.
33
34
 
34
35
  - Formats: `.mp4`, `.mov`, `.m4v`, `.webm`. Upload the finished master;
35
36
  VOD transcodes to MP4 for playback.
36
- - The whole file goes up in one request — fine for episodes (hundreds of MB).
37
- Multi-GB masters are better uploaded from the web, which chunks them.
37
+ - The whole file goes up in one request. The cap is 500 MB, with no duration limit.
38
38
  - A credential lasts 15 minutes and the server allows 30 per 10 minutes —
39
39
  upload a season one episode at a time, not in parallel.
40
40
  - `--vid <vid>` attaches a video already in the space (e.g. uploaded on the
@@ -43,7 +43,7 @@ transcoding), then saves the episode list with the new `vodVid`.
43
43
  ### Legacy path
44
44
 
45
45
  `--legacy` (or `--asset <assetId>`) stores the episode as a plain WJI asset
46
- (≤200 MB) with a permanent public URL. It plays immediately with no
46
+ (≤500 MB) with a permanent public URL. It plays immediately with no
47
47
  transcoding, but anyone who sees the URL can share it, and tier locks only hide
48
48
  it on the page. Use VOD for anything gated; legacy only for short public
49
49
  extras.