@wji.ai/cli 0.9.0 → 0.9.2
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.
- package/README.md +7 -1
- package/dist/index.js +156 -11
- package/package.json +1 -1
- package/skill/backreel/SKILL.md +6 -2
- package/skill/backreel/reference/episodes.md +45 -7
- package/skill/short-drama/SKILL.md +27 -4
- package/skill/short-drama/examples/night-shift/episodes/E01.json +173 -1
- package/skill/short-drama/examples/night-shift/project.json +6 -0
- package/skill/short-drama/reference/dialogue-craft.md +16 -11
- package/skill/short-drama/reference/prompt-audit.mjs +308 -0
- package/skill/short-drama/reference/reference-binding.md +84 -0
- package/skill/short-drama/reference/schema.md +14 -5
- package/skill/short-drama/reference/stage-characters.md +1 -1
- package/skill/short-drama/reference/stage-script.md +54 -7
- package/skill/short-drama/reference/stage-storyboard.md +29 -24
- package/skill/short-drama/scripts/selftest.mjs +154 -9
- package/skill/short-drama/scripts/short-drama.mjs +467 -23
- package/skill/w-film-dialogue-editor/CHANGELOG.md +44 -0
- package/skill/w-film-dialogue-editor/SKILL.md +124 -0
- package/skill/w-film-dialogue-editor/references/data/README.md +222 -0
- package/skill/w-film-dialogue-editor/references/data/en/core.json +1666 -0
- package/skill/w-film-dialogue-editor/references/data/en/slang.json +1602 -0
- package/skill/w-film-dialogue-editor/references/data/en/work.json +1722 -0
- package/skill/w-film-dialogue-editor/references/data/en-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/en-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/ja/core.json +1978 -0
- package/skill/w-film-dialogue-editor/references/data/ja/slang.json +1927 -0
- package/skill/w-film-dialogue-editor/references/data/ja/work.json +1972 -0
- package/skill/w-film-dialogue-editor/references/data/ja-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/ja-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/ko/core.json +1839 -0
- package/skill/w-film-dialogue-editor/references/data/ko/slang.json +1897 -0
- package/skill/w-film-dialogue-editor/references/data/ko/work.json +1829 -0
- package/skill/w-film-dialogue-editor/references/data/ko-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/ko-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/core.json +1794 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/slang.json +1609 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/work.json +1648 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/core.json +2476 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/gen-z.json +3263 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/older.json +3632 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/rough.json +1668 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/workplace.json +3697 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.csv +373 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.md +391 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/core.json +1640 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/slang.json +1537 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/work.json +1699 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.csv +132 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.md +144 -0
- package/skill/w-film-dialogue-editor/references/dialogue_revision_framework.md +84 -0
- package/skill/w-film-dialogue-editor/references/en-index.md +265 -0
- package/skill/w-film-dialogue-editor/references/ja-index.md +633 -0
- package/skill/w-film-dialogue-editor/references/ko-index.md +498 -0
- package/skill/w-film-dialogue-editor/references/lang-en.md +242 -0
- package/skill/w-film-dialogue-editor/references/lang-ja.md +312 -0
- package/skill/w-film-dialogue-editor/references/lang-ko.md +290 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-cn.md +60 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-hk-mandarin.md +34 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-hk.md +306 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-period.md +38 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-tw.md +57 -0
- package/skill/w-film-dialogue-editor/references/zh-cn-index.md +636 -0
- package/skill/w-film-dialogue-editor/references/zh-hk-index.md +1450 -0
- package/skill/w-film-dialogue-editor/references/zh-tw-index.md +457 -0
- package/skill/w-film-dialogue-editor/scripts/rebuild.sh +13 -0
- package/skill/w-film-dialogue-editor/scripts/ref.mjs +539 -0
- package/skill/w-film-dialogue-editor/templates/dialogue_revision_report.md +43 -0
package/README.md
CHANGED
|
@@ -215,6 +215,7 @@ wji backreel create "Harbour" -d @synopsis.txt --cover <assetId>
|
|
|
215
215
|
wji backreel credit add <postId> --npc @mika --role "飾 阿明" --job appearance
|
|
216
216
|
wji backreel image poster.png --slot portrait --post <postId> # size check + upload + set
|
|
217
217
|
wji backreel episode add <postId> E01.mp4 -t "第一集" # VOD upload
|
|
218
|
+
wji backreel episode subs add <postId> 1 E01.zh-Hant.srt # subtitles, one per language
|
|
218
219
|
wji backreel check <postId>
|
|
219
220
|
wji backreel publish <postId>
|
|
220
221
|
wji backreel enquiry list --unread
|
|
@@ -310,10 +311,15 @@ generations, banner sizes, AI-disclosure compliance); `social-media` holds
|
|
|
310
311
|
every platform's image / video specs (Instagram, TikTok, YouTube, Facebook, X,
|
|
311
312
|
LinkedIn, Pinterest, Threads, Snapchat, 小红书, 抖音, 快手, 视频号, B站, 微博,
|
|
312
313
|
公众号) and the repurposing recipes — an episode into 9:16 teasers and posters,
|
|
313
|
-
an ad master into every placement.
|
|
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:
|
|
314
319
|
|
|
315
320
|
```bash
|
|
316
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
|
|
317
323
|
cp -r "$(npm root -g)/@wji.ai/cli/skill/ads" ~/.claude/skills/ads
|
|
318
324
|
cp -r "$(npm root -g)/@wji.ai/cli/skill/social-media" ~/.claude/skills/social-media
|
|
319
325
|
```
|
package/dist/index.js
CHANGED
|
@@ -281,6 +281,8 @@ var MIME_TYPES = {
|
|
|
281
281
|
mp3: "audio/mpeg",
|
|
282
282
|
wav: "audio/wav",
|
|
283
283
|
m4a: "audio/mp4",
|
|
284
|
+
srt: "application/x-subrip",
|
|
285
|
+
vtt: "text/vtt",
|
|
284
286
|
pdf: "application/pdf",
|
|
285
287
|
csv: "text/csv",
|
|
286
288
|
txt: "text/plain",
|
|
@@ -6347,9 +6349,10 @@ async function uploadToVod(token, file, onProgress) {
|
|
|
6347
6349
|
// src/commands/backreel-episode.ts
|
|
6348
6350
|
var MAX_EPISODES = 30;
|
|
6349
6351
|
var VIDEO_EXTS = [".mp4", ".mov", ".m4v", ".webm"];
|
|
6350
|
-
var
|
|
6352
|
+
var VIDEO_MAX_BYTES = 500 * 1024 * 1024;
|
|
6353
|
+
var subtitlesOf = (e) => (e.subtitles || []).map((s) => ({ id: s.id, lang: s.lang, ...s.label ? { label: s.label } : {} }));
|
|
6351
6354
|
function toDto(e, order) {
|
|
6352
|
-
const base = { title: e.title, order, tier: e.tier || "public" };
|
|
6355
|
+
const base = { title: e.title, order, tier: e.tier || "public", subtitles: subtitlesOf(e) };
|
|
6353
6356
|
return e.vodVid ? { ...base, vodVid: e.vodVid } : { ...base, video: idOf2(e.video) };
|
|
6354
6357
|
}
|
|
6355
6358
|
async function loadEpisodes(client, postId) {
|
|
@@ -6373,8 +6376,8 @@ async function resolveSource(client, opts, postId, file) {
|
|
|
6373
6376
|
const ext = extname2(file).toLowerCase();
|
|
6374
6377
|
if (!VIDEO_EXTS.includes(ext)) throw new Error(`${basename6(file)}: episodes must be ${VIDEO_EXTS.join(" / ")}`);
|
|
6375
6378
|
const size = (await stat5(file)).size;
|
|
6379
|
+
if (size > VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 500 MB`);
|
|
6376
6380
|
if (opts.legacy) {
|
|
6377
|
-
if (size > ASSET_VIDEO_MAX_BYTES) throw new Error(`${basename6(file)} is over 200 MB \u2014 drop --legacy to upload it to VOD`);
|
|
6378
6381
|
info(color.dim(`Uploading ${basename6(file)} as a WJI asset\u2026`));
|
|
6379
6382
|
const asset = await client.upload("/assets/v2/upload", file, {});
|
|
6380
6383
|
return { video: asset.id };
|
|
@@ -6393,10 +6396,63 @@ function printEpisodes(post) {
|
|
|
6393
6396
|
{ header: "tier", value: (e) => e.tier },
|
|
6394
6397
|
{ header: "source", value: (e) => e.vodVid ? `vod ${e.vodVid}` : e.video ? `asset ${idOf2(e.video)}` : "-" },
|
|
6395
6398
|
{ header: "status", value: (e) => e.vodVid ? e.vodStatus || "?" : e.video ? "ready" : "" },
|
|
6396
|
-
{ header: "duration", value: (e) => e.durationSec ? `${Math.round(e.durationSec)}s` : "" }
|
|
6399
|
+
{ header: "duration", value: (e) => e.durationSec ? `${Math.round(e.durationSec)}s` : "" },
|
|
6400
|
+
{ header: "subs", value: (e) => (e.subtitles || []).map((s) => s.lang).join(" ") }
|
|
6397
6401
|
]);
|
|
6398
6402
|
}
|
|
6399
|
-
var
|
|
6403
|
+
var MAX_SUBTITLES = 8;
|
|
6404
|
+
var SUBTITLE_EXTS = [".srt", ".vtt"];
|
|
6405
|
+
var LANG_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8}){0,3}$/;
|
|
6406
|
+
function guessSubtitleLang(fileName) {
|
|
6407
|
+
const padded = `.${basename6(fileName).toLowerCase().replace(/\.(srt|vtt)$/, "")}.`;
|
|
6408
|
+
const hints = [
|
|
6409
|
+
[/(zh[-_]?(hant|tw|hk)|繁|cht)/, "zh-Hant"],
|
|
6410
|
+
[/(zh[-_]?(hans|cn)|简|chs)/, "zh-Hans"],
|
|
6411
|
+
[/(yue|粵|粤)/, "yue"],
|
|
6412
|
+
[/[._ -](en|eng|english)[._ -]/, "en"],
|
|
6413
|
+
[/[._ -](ja|jp|jpn)[._ -]/, "ja"],
|
|
6414
|
+
[/[._ -](ko|kr|kor)[._ -]/, "ko"],
|
|
6415
|
+
[/[._ -](th|tha)[._ -]/, "th"],
|
|
6416
|
+
[/[._ -](vi|vie)[._ -]/, "vi"]
|
|
6417
|
+
];
|
|
6418
|
+
return hints.find(([re]) => re.test(padded))?.[1];
|
|
6419
|
+
}
|
|
6420
|
+
function printSubtitles(episode) {
|
|
6421
|
+
const subs = episode.subtitles || [];
|
|
6422
|
+
if (!subs.length) return info(`Episode ${(episode.order ?? 0) + 1} has no subtitles`);
|
|
6423
|
+
table(subs, [
|
|
6424
|
+
{ header: "lang", value: (s) => s.lang },
|
|
6425
|
+
{ header: "label", value: (s) => s.label || "" },
|
|
6426
|
+
{ header: "default", value: (s) => s === subs[0] ? color.green("\u2605") : "" },
|
|
6427
|
+
{ header: "id", value: (s) => color.dim(s.id) }
|
|
6428
|
+
]);
|
|
6429
|
+
}
|
|
6430
|
+
async function saveSubtitles(client, postId, episodes, i, message) {
|
|
6431
|
+
const updated = await saveEpisodes(client, postId, episodes);
|
|
6432
|
+
const episode = [...updated.episodes || []].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i];
|
|
6433
|
+
render(episode?.subtitles || [], () => {
|
|
6434
|
+
success(message);
|
|
6435
|
+
printSubtitles(episode);
|
|
6436
|
+
});
|
|
6437
|
+
}
|
|
6438
|
+
function findTrack(subs, lang, i) {
|
|
6439
|
+
const at = subs.findIndex((s) => s.lang.toLowerCase() === lang.toLowerCase());
|
|
6440
|
+
if (at < 0) {
|
|
6441
|
+
const have = subs.map((s) => s.lang).join(", ") || "none";
|
|
6442
|
+
throw new Error(`Episode ${i + 1} has no ${lang} subtitles (has: ${have})`);
|
|
6443
|
+
}
|
|
6444
|
+
return at;
|
|
6445
|
+
}
|
|
6446
|
+
function pickRendition(renditions, want) {
|
|
6447
|
+
const w = want.toLowerCase();
|
|
6448
|
+
const hit = renditions.find((r) => String(r.definition || "").toLowerCase() === w) || renditions.find((r) => r.height && String(r.height) === w.replace(/p$/, ""));
|
|
6449
|
+
if (!hit) {
|
|
6450
|
+
const have = renditions.map((r) => r.definition || `${r.height}p`).join(", ") || "only the default";
|
|
6451
|
+
throw new Error(`No ${want} rendition (available: ${have})`);
|
|
6452
|
+
}
|
|
6453
|
+
return hit;
|
|
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 (\u2264500 MB) instead of VOD");
|
|
6400
6456
|
function episodeCommand(globals2) {
|
|
6401
6457
|
const ep = new Command19("episode").aliases(["episodes", "ep"]).description("Episodes of a showcase post: upload, reorder, gate by tier, check transcoding");
|
|
6402
6458
|
ep.command("list <postId>", { isDefault: true }).description("List the episodes with their tier, source and VOD status").action(async (postId) => {
|
|
@@ -6417,7 +6473,7 @@ function episodeCommand(globals2) {
|
|
|
6417
6473
|
const at = opts.at === void 0 ? episodes.length : episodeIndex(opts.at, episodes.length + 1);
|
|
6418
6474
|
const source = await resolveSource(client, opts, postId, file);
|
|
6419
6475
|
if (!source) throw new Error("Give the video: a file, --vid <vid> or --asset <assetId>");
|
|
6420
|
-
episodes.splice(at, 0, { title: opts.title, order: at, tier, ...source });
|
|
6476
|
+
episodes.splice(at, 0, { title: opts.title, order: at, tier, subtitles: [], ...source });
|
|
6421
6477
|
const updated = await saveEpisodes(client, postId, episodes);
|
|
6422
6478
|
render(updated.episodes, () => {
|
|
6423
6479
|
success(`Added episode ${at + 1} "${opts.title}" to "${updated.title}"`);
|
|
@@ -6433,12 +6489,21 @@ function episodeCommand(globals2) {
|
|
|
6433
6489
|
const i = episodeIndex(n, episodes.length);
|
|
6434
6490
|
const source = await resolveSource(client, { ...opts, title: opts.title || episodes[i].title }, postId, file);
|
|
6435
6491
|
if (!opts.title && !opts.tier && !source) throw new Error("Nothing to change \u2014 pass --title, --tier or a new video");
|
|
6436
|
-
const next = {
|
|
6492
|
+
const next = {
|
|
6493
|
+
title: opts.title || episodes[i].title,
|
|
6494
|
+
order: i,
|
|
6495
|
+
tier: opts.tier ? oneOf2(opts.tier, TIERS, "--tier") : episodes[i].tier,
|
|
6496
|
+
// a new cut of the same episode usually keeps its timing, so the tracks stay
|
|
6497
|
+
subtitles: episodes[i].subtitles
|
|
6498
|
+
};
|
|
6437
6499
|
episodes[i] = source ? { ...next, ...source } : { ...episodes[i], ...next };
|
|
6438
6500
|
const updated = await saveEpisodes(client, postId, episodes);
|
|
6439
6501
|
render(updated.episodes, () => {
|
|
6440
6502
|
success(`Updated episode ${i + 1}`);
|
|
6441
6503
|
printEpisodes(updated);
|
|
6504
|
+
if (source && next.subtitles.length) {
|
|
6505
|
+
info(color.dim(`Kept ${next.subtitles.length} subtitle track(s) \u2014 \`wji br episode subs rm\` any that no longer line up`));
|
|
6506
|
+
}
|
|
6442
6507
|
});
|
|
6443
6508
|
});
|
|
6444
6509
|
ep.command("rm <postId> <n...>").description("Remove episodes (later ones move up)").option("-y, --yes", "skip confirmation").action(async (postId, ns, opts) => {
|
|
@@ -6513,18 +6578,99 @@ function episodeCommand(globals2) {
|
|
|
6513
6578
|
if (rows.some((r) => r.status === "failed")) warn("A failed transcode needs a new upload: `episode set <post> <n> <file>`");
|
|
6514
6579
|
});
|
|
6515
6580
|
});
|
|
6516
|
-
ep.command("play <postId> <n>").description("Print a short-lived play URL for episode n (as the owner)").action(async (postId, n) => {
|
|
6581
|
+
ep.command("play <postId> <n>").description("Print a short-lived play URL for episode n (as the owner) \u2014 the best quality unless --quality").option("-q, --quality <q>", "a rendition such as 720p or 480p").option("--qualities", "list every rendition instead of printing one URL").action(async (postId, n, opts) => {
|
|
6517
6582
|
const client = backreelClient(globals2);
|
|
6518
6583
|
const post = await loadPost(client, postId);
|
|
6519
6584
|
const i = episodeIndex(n, (post.episodes || []).length);
|
|
6520
6585
|
const order = [...post.episodes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i].order ?? i;
|
|
6521
6586
|
const play = await client.get(`${POSTS}/list/${postId}/episodes/${order}/play`);
|
|
6522
|
-
|
|
6587
|
+
const renditions = play.renditions || [];
|
|
6588
|
+
if (opts.qualities) {
|
|
6589
|
+
return render(renditions, (rows) => {
|
|
6590
|
+
if (!rows.length) return info("Only one version (legacy asset, or VOD returned no renditions)");
|
|
6591
|
+
table(rows, [
|
|
6592
|
+
{ header: "quality", value: (r) => r.definition || "" },
|
|
6593
|
+
{ header: "size", value: (r) => r.width && r.height ? `${r.width}\xD7${r.height}` : "" },
|
|
6594
|
+
{ header: "bitrate", value: (r) => r.bitrate ? `${Math.round(r.bitrate / 1e3)} kbps` : "" }
|
|
6595
|
+
]);
|
|
6596
|
+
if (play.expiresAt) info(color.dim(`URLs expire ${play.expiresAt} \u2014 --json to get them`));
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
const url = opts.quality ? pickRendition(renditions, opts.quality).url : play.url;
|
|
6600
|
+
render({ ...play, url }, (p) => {
|
|
6523
6601
|
process.stdout.write(`${p.url}
|
|
6524
6602
|
`);
|
|
6525
6603
|
if (p.expiresAt) info(color.dim(`expires ${p.expiresAt}`));
|
|
6526
6604
|
});
|
|
6527
6605
|
});
|
|
6606
|
+
const subs = ep.command("subs").aliases(["subtitles", "sub"]).description("Subtitle tracks of an episode: .srt / .vtt, one per language, \u22648, the first is the default");
|
|
6607
|
+
subs.command("list <postId> <n>", { isDefault: true }).description("List episode n's subtitle tracks").action(async (postId, n) => {
|
|
6608
|
+
const client = backreelClient(globals2);
|
|
6609
|
+
const post = await loadPost(client, postId);
|
|
6610
|
+
const i = episodeIndex(n, (post.episodes || []).length);
|
|
6611
|
+
const episode = [...post.episodes].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))[i];
|
|
6612
|
+
render(episode.subtitles || [], () => printSubtitles(episode));
|
|
6613
|
+
});
|
|
6614
|
+
subs.command("add <postId> <n> <file>").description("Upload a .srt / .vtt and attach it to episode n").option("-l, --lang <lang>", "BCP 47 language: en, ja, ko, zh-Hant, zh-Hans, yue\u2026 (guessed from the file name if omitted)").option("--label <label>", "name shown in the player (default: the language's own name)").option("--default", "make it the default track (first in the list)").option("--replace", "replace the episode's existing track in that language").action(async (postId, n, file, opts) => {
|
|
6615
|
+
const client = backreelClient(globals2);
|
|
6616
|
+
const ext = extname2(file).toLowerCase();
|
|
6617
|
+
if (!SUBTITLE_EXTS.includes(ext)) throw new Error(`${basename6(file)}: subtitles must be .srt or .vtt`);
|
|
6618
|
+
if ((await stat5(file)).size > 1024 * 1024) throw new Error(`${basename6(file)} is over 1 MB`);
|
|
6619
|
+
const lang = opts.lang || guessSubtitleLang(file);
|
|
6620
|
+
if (!lang) throw new Error(`Can't tell the language from "${basename6(file)}" \u2014 pass --lang (e.g. en, zh-Hant)`);
|
|
6621
|
+
if (!LANG_RE.test(lang)) throw new Error(`--lang ${lang} isn't a language code like en, ja or zh-Hant`);
|
|
6622
|
+
const label = opts.label ? String(opts.label).trim() : void 0;
|
|
6623
|
+
if (label && label.length > 40) throw new Error("--label is at most 40 characters");
|
|
6624
|
+
const { post, episodes } = await loadEpisodes(client, postId);
|
|
6625
|
+
const i = episodeIndex(n, episodes.length);
|
|
6626
|
+
const duration = [...post.episodes].sort((x, y) => (x.order ?? 0) - (y.order ?? 0))[i]?.durationSec;
|
|
6627
|
+
const list = [...episodes[i].subtitles];
|
|
6628
|
+
const existing = list.findIndex((s) => s.lang.toLowerCase() === lang.toLowerCase());
|
|
6629
|
+
if (existing >= 0 && !opts.replace) {
|
|
6630
|
+
throw new Error(`Episode ${i + 1} already has ${lang} subtitles \u2014 add --replace to swap them`);
|
|
6631
|
+
}
|
|
6632
|
+
if (existing < 0 && list.length >= MAX_SUBTITLES) {
|
|
6633
|
+
throw new Error(`Episode ${i + 1} already has ${MAX_SUBTITLES} subtitle tracks`);
|
|
6634
|
+
}
|
|
6635
|
+
const uploaded = await client.upload("/backreel/subtitle/upload", file, {});
|
|
6636
|
+
const keptLabel = label ?? (existing >= 0 ? list[existing].label : void 0);
|
|
6637
|
+
const track = { id: uploaded.id, lang, ...keptLabel ? { label: keptLabel } : {} };
|
|
6638
|
+
if (existing >= 0) list.splice(existing, 1, track);
|
|
6639
|
+
else list.push(track);
|
|
6640
|
+
if (opts.default) list.unshift(...list.splice(list.indexOf(track), 1));
|
|
6641
|
+
episodes[i].subtitles = list;
|
|
6642
|
+
await saveSubtitles(client, postId, episodes, i, `${existing >= 0 ? "Replaced" : "Added"} ${lang} subtitles on episode ${i + 1} (${uploaded.cueCount} lines)`);
|
|
6643
|
+
if (duration && uploaded.lastCueSec > duration + 2) {
|
|
6644
|
+
warn(`The last line is at ${Math.round(uploaded.lastCueSec)}s but the episode is ${Math.round(duration)}s \u2014 is this the right file?`);
|
|
6645
|
+
}
|
|
6646
|
+
});
|
|
6647
|
+
subs.command("rm <postId> <n> <lang...>").description("Remove episode n's tracks in these languages").action(async (postId, n, langs) => {
|
|
6648
|
+
const client = backreelClient(globals2);
|
|
6649
|
+
const { episodes } = await loadEpisodes(client, postId);
|
|
6650
|
+
const i = episodeIndex(n, episodes.length);
|
|
6651
|
+
const drop = new Set(langs.map((l) => findTrack(episodes[i].subtitles, l, i)));
|
|
6652
|
+
episodes[i].subtitles = episodes[i].subtitles.filter((_, at) => !drop.has(at));
|
|
6653
|
+
await saveSubtitles(client, postId, episodes, i, `Removed ${langs.join(", ")} from episode ${i + 1}`);
|
|
6654
|
+
});
|
|
6655
|
+
subs.command("default <postId> <n> <lang>").description("Make a track the default (moves it first)").action(async (postId, n, lang) => {
|
|
6656
|
+
const client = backreelClient(globals2);
|
|
6657
|
+
const { episodes } = await loadEpisodes(client, postId);
|
|
6658
|
+
const i = episodeIndex(n, episodes.length);
|
|
6659
|
+
const list = [...episodes[i].subtitles];
|
|
6660
|
+
list.unshift(...list.splice(findTrack(list, lang, i), 1));
|
|
6661
|
+
episodes[i].subtitles = list;
|
|
6662
|
+
await saveSubtitles(client, postId, episodes, i, `${list[0].lang} is now episode ${i + 1}'s default`);
|
|
6663
|
+
});
|
|
6664
|
+
subs.command("label <postId> <n> <lang> [label]").description("Set the name the player shows for a track (omit it to go back to the language name)").action(async (postId, n, lang, label) => {
|
|
6665
|
+
const client = backreelClient(globals2);
|
|
6666
|
+
if (label && label.trim().length > 40) throw new Error("A label is at most 40 characters");
|
|
6667
|
+
const { episodes } = await loadEpisodes(client, postId);
|
|
6668
|
+
const i = episodeIndex(n, episodes.length);
|
|
6669
|
+
const at = findTrack(episodes[i].subtitles, lang, i);
|
|
6670
|
+
const { label: _old, ...rest } = episodes[i].subtitles[at];
|
|
6671
|
+
episodes[i].subtitles[at] = label?.trim() ? { ...rest, label: label.trim() } : rest;
|
|
6672
|
+
await saveSubtitles(client, postId, episodes, i, `Updated the ${rest.lang} label on episode ${i + 1}`);
|
|
6673
|
+
});
|
|
6528
6674
|
return ep;
|
|
6529
6675
|
}
|
|
6530
6676
|
|
|
@@ -7846,7 +7992,6 @@ function backreelCommand(globals2) {
|
|
|
7846
7992
|
if (!(p.credits || []).length) advice.push("no credits");
|
|
7847
7993
|
const pending = episodes.filter((e) => e.vodVid && e.vodStatus !== "ready");
|
|
7848
7994
|
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\``);
|
|
7849
|
-
if (p.trailerDurationSec && p.trailerDurationSec > 31) advice.push(`trailer is ${p.trailerDurationSec}s; the limit is 30s`);
|
|
7850
7995
|
if (p.donation?.enabled && p.kind === "crowdfunding" && p.campaign?.deadline && new Date(p.campaign.deadline) < /* @__PURE__ */ new Date()) {
|
|
7851
7996
|
advice.push("the campaign deadline has passed \u2014 donations are closed");
|
|
7852
7997
|
}
|
|
@@ -7924,7 +8069,7 @@ function backreelCommand(globals2) {
|
|
|
7924
8069
|
return br;
|
|
7925
8070
|
}
|
|
7926
8071
|
function postOptions(cmd) {
|
|
7927
|
-
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 (\
|
|
8072
|
+
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("--body <json>", "any other post fields as JSON (@file / - supported); flags win");
|
|
7928
8073
|
}
|
|
7929
8074
|
async function postBody(opts) {
|
|
7930
8075
|
const body = { ...await readJson(opts.body) || {} };
|
package/package.json
CHANGED
package/skill/backreel/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ 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 ≤
|
|
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
32
|
| campaign + donations | goal, offline total, deadline, donation tiers, offline supporters | `br fund …` |
|
|
@@ -97,6 +97,10 @@ wji br episode set <postId> 3 E03_v2.mp4 # replace the video
|
|
|
97
97
|
wji br episode move <postId> 4 2
|
|
98
98
|
wji br episode rm <postId> 6 -y
|
|
99
99
|
wji br episode play <postId> 1 # short-lived URL to watch it yourself
|
|
100
|
+
wji br episode play <postId> 1 --qualities # renditions VOD made (1080p / 720p / …)
|
|
101
|
+
wji br episode subs add <postId> 1 E01.zh-Hant.srt # language guessed from the name
|
|
102
|
+
wji br episode subs add <postId> 1 E01_en.vtt --lang en --default
|
|
103
|
+
wji br episode subs <postId> 1 # list tracks; the first is the default
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
Episodes exist on **showcase** posts only (≤30). A local file goes straight
|
|
@@ -105,7 +109,7 @@ nothing on the page can be copied and shared. Transcoding takes a few minutes;
|
|
|
105
109
|
an episode stays `processing` until then and viewers get "still
|
|
106
110
|
transcoding". Numbers on the command line are 1-based positions. Read
|
|
107
111
|
[reference/episodes.md](reference/episodes.md) for the trailer, the legacy
|
|
108
|
-
asset path, tier strategy and failure handling.
|
|
112
|
+
asset path, tier strategy, subtitles and failure handling.
|
|
109
113
|
|
|
110
114
|
## 4. Crowdfunding and donations
|
|
111
115
|
|
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| posts | any | **showcase only** (switching to crowdfunding drops them) |
|
|
8
8
|
| count | one | ≤30 |
|
|
9
|
-
| length |
|
|
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 # ≤
|
|
17
|
+
wji asset upload trailer.mp4 --json # ≤500 MB, any length
|
|
17
18
|
wji br set <postId> --trailer <assetId>
|
|
18
19
|
```
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
(≤
|
|
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.
|
|
@@ -78,6 +78,44 @@ wji br episode rm <postId> 7 8 -y
|
|
|
78
78
|
Removing an episode does not delete the video from VOD; re-adding it later
|
|
79
79
|
with `--vid` works.
|
|
80
80
|
|
|
81
|
+
## Subtitles
|
|
82
|
+
|
|
83
|
+
Each episode takes up to 8 subtitle tracks, one per language, from `.srt` or
|
|
84
|
+
`.vtt` files (≤1 MB). The server cleans them into plain WebVTT — italics,
|
|
85
|
+
colours and `{\an8}` positioning are dropped. The **first** track is the
|
|
86
|
+
default; viewers who never chose one get the track matching the site's
|
|
87
|
+
language, else the default.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
wji br episode subs add <postId> 2 E02.zh-Hant.srt # zh-Hant guessed from the name
|
|
91
|
+
wji br episode subs add <postId> 2 E02.srt --lang en --label "English (CC)"
|
|
92
|
+
wji br episode subs add <postId> 2 E02_en_v2.srt --lang en --replace
|
|
93
|
+
wji br episode subs default <postId> 2 en
|
|
94
|
+
wji br episode subs label <postId> 2 en # back to the language name
|
|
95
|
+
wji br episode subs rm <postId> 2 ja ko
|
|
96
|
+
wji br episode subs <postId> 2
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Languages are BCP 47 tags: `zh-Hant`, `zh-Hans`, `yue`, `en`, `ja`, `ko`,
|
|
100
|
+
`th`, `vi`, `id`, `ms`, `es`, `pt`, `fr`, `de` (others are accepted, but the
|
|
101
|
+
player shows the raw code). A file the server can't read fails at upload,
|
|
102
|
+
before the episode changes. `add` warns when the last line runs past the
|
|
103
|
+
episode's length — usually the wrong episode's file.
|
|
104
|
+
|
|
105
|
+
Subtitles follow the episode's tier: a locked episode never serves its text.
|
|
106
|
+
Replacing the video (`episode set <post> <n> new.mp4`) keeps the tracks; remove
|
|
107
|
+
them if the new cut changed the timing.
|
|
108
|
+
|
|
109
|
+
## Quality
|
|
110
|
+
|
|
111
|
+
VOD transcodes each upload into the renditions its template defines. The
|
|
112
|
+
player offers Auto plus each one; with a single rendition the menu hides.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
wji br episode play <postId> 1 --qualities # what VOD made
|
|
116
|
+
wji br episode play <postId> 1 -q 720p # URL of one rendition
|
|
117
|
+
```
|
|
118
|
+
|
|
81
119
|
## Who can watch: tiers
|
|
82
120
|
|
|
83
121
|
`public` anyone · `member` signed in · `supporter` has donated to this post ·
|
|
@@ -36,8 +36,11 @@ Either way, this skill only says **what to build**; the operations doc says how
|
|
|
36
36
|
|
|
37
37
|
**How the drama is written** comes from `{baseDir}/reference/craft-story.md` (the 短剧 method:
|
|
38
38
|
brief, three phases, 分集目录, 爽点 / 钩子 libraries, the episode formula, the character bible) and
|
|
39
|
-
`{baseDir}/reference/dialogue-craft.md` (lines as action, 语体 rules, the
|
|
39
|
+
`{baseDir}/reference/dialogue-craft.md` (lines as action, 语体 rules, the dialogue review). Read the
|
|
40
40
|
first before stage 1 and the second before stage 4. The gates encode what those two can check.
|
|
41
|
+
The full dialogue method is the user's **`w-film-dialogue-editor`** skill, installed beside this
|
|
42
|
+
one: its language guides and corpus are read while the lines are written and again in the required
|
|
43
|
+
dialogue review after the script.
|
|
41
44
|
|
|
42
45
|
**Re-read the guideline.** The 创作指引 (`render guideline`) is the drama's top rule document.
|
|
43
46
|
Print it before writing any character, any episode and any storyboard, and print the speakers'
|
|
@@ -277,6 +280,7 @@ Read `{baseDir}/reference/stage-script.md`.
|
|
|
277
280
|
writes `episodes/E0N.json` with target seconds, hook/cliff, claimed beats, the episode design
|
|
278
281
|
(`function` / `task` / `change` / `hookType`, `satisfactionBeat` to set) and a `seedNote`
|
|
279
282
|
(synopsis, the 爽点 to realise, register, guideline priorities, every speaker's `bible.speech`,
|
|
283
|
+
the dialogue skill's guides and per-speaker corpus queries in `dialogue`,
|
|
280
284
|
ids, previous cliff, budget). Fill `scenes[].flow` — action beats and line beats alternating,
|
|
281
285
|
**common actions only**, hook image inside the first 3 beats and by 15 s, a `turn: true` beat
|
|
282
286
|
at least every 30 s, the 爽点 on an action (`satisfactionBeat`), cliff as the last beat; lines
|
|
@@ -286,9 +290,13 @@ Read `{baseDir}/reference/stage-script.md`.
|
|
|
286
290
|
Delete `seedNote` when done.
|
|
287
291
|
2. `validate --stage script --ep N` — duration math, line length, speakers, hook window, the
|
|
288
292
|
rhythm, the design, filmability, upstream ids; the register gate only advises. Over budget: cut
|
|
289
|
-
actions first; under: add conflict, not small talk.
|
|
290
|
-
|
|
291
|
-
|
|
293
|
+
actions first; under: add conflict, not small talk.
|
|
294
|
+
3. **Dialogue review** (required, gate `dialogue-review`): `seed dialogue --ep N` → read the
|
|
295
|
+
`w-film-dialogue-editor` framework and the guide of each 语体 it prints, query the corpus per
|
|
296
|
+
speaker → fill the diagnosis, level, one row per line (issue / revised / reason / playable
|
|
297
|
+
note) and the guide's 自检 → `apply dialogue --ep N` → re-validate. A line edited later is
|
|
298
|
+
reviewed again. Details in `stage-script.md`.
|
|
299
|
+
4. Sync: `render script --ep N` (the 短剧 episode format with the 创作自检 table) → `story/` dataset
|
|
292
300
|
`E0N 剧本` (type `screenplay`, `viewMode=content`); bind `wji.screenplayDatasetId` with `--ep N`.
|
|
293
301
|
|
|
294
302
|
## Stage 5 — Storyboard (per episode)
|
|
@@ -307,6 +315,21 @@ prompt, alongside the camera package and each speaker's delivery.
|
|
|
307
315
|
|
|
308
316
|
**Two people talking** (gate `two-person-framing`, sheets mode): a two-shot once, then over-the-shoulder (`ots`) for the exchange, a clean single for the punchline. A single names only its person; every look states a screen direction (`eyeline`); the sides stay locked. See `reference/craft-shots.md`.
|
|
309
317
|
|
|
318
|
+
**Every picture says what it is, and what NOT to copy from it** (gate `ref-binding`, sheets mode).
|
|
319
|
+
The model reads one string: a legend line that only says "a character sheet" for two different
|
|
320
|
+
people is how faces get swapped, and a shot that never names its `@imageK` is a shot where the
|
|
321
|
+
model invents the face or the prop. So — one legend line per picture, naming a person by age /
|
|
322
|
+
hair / clothes from the character bible, the room by its name, a prop by its name; every picture
|
|
323
|
+
that is **not** a person also says what may not be copied from it (*take only its light ratios,
|
|
324
|
+
contrast and colour temperature — never its composition or its people*); the room is named once
|
|
325
|
+
for the whole segment (`Every shot stays inside @image1 …`); every person and every prop is bound
|
|
326
|
+
by its label again **inside its own shot**; no connected picture may go unused; and a segment may
|
|
327
|
+
not carry more pictures than the model takes (`refs` cap). Wording patterns and the six roles:
|
|
328
|
+
`reference/reference-binding.md`. A prompt that was written or edited by hand (or copied from a
|
|
329
|
+
production run) can be proofread before any image is spent:
|
|
330
|
+
`node {baseDir}/reference/prompt-audit.mjs --project <project> --ep 1 --segment E0N-k --prompt <file>`
|
|
331
|
+
— it reports per-shot unbound people and props, indistinguishable legend lines and unused pictures.
|
|
332
|
+
|
|
310
333
|
**Every cut is lit and composed in the preset's look.** Read the preset's guide
|
|
311
334
|
(`{baseDir}/reference/cinematography-<preset>.md`) before the first storyboard. Name the source
|
|
312
335
|
in `light.source`, stage the person against it, and choose the camera side that gives the
|
|
@@ -1879,5 +1879,177 @@
|
|
|
1879
1879
|
"canvasId": "",
|
|
1880
1880
|
"shotFolderId": "",
|
|
1881
1881
|
"displayNodes": {}
|
|
1882
|
+
},
|
|
1883
|
+
"dialogueReview": {
|
|
1884
|
+
"level": "轻修",
|
|
1885
|
+
"guides": [
|
|
1886
|
+
"lang-zh-cn.md"
|
|
1887
|
+
],
|
|
1888
|
+
"diagnosis": {
|
|
1889
|
+
"goal": "小夏想守住别人托付的袋子;老陈想弄清袋子从哪来",
|
|
1890
|
+
"conflict": "老陈步步试探,小夏用「别人的东西」挡住",
|
|
1891
|
+
"subtext": "老陈认得穿雨衣的人,却不说破;小夏开始怀疑他",
|
|
1892
|
+
"voice": "小夏短句、收得急;老陈慢、一字一顿,遮住名字也分得出",
|
|
1893
|
+
"rhythm": "问答都短,每两三句就被动作打断,节奏成立"
|
|
1894
|
+
},
|
|
1895
|
+
"rows": [
|
|
1896
|
+
{
|
|
1897
|
+
"scene": 1,
|
|
1898
|
+
"beat": 2,
|
|
1899
|
+
"speaker": "C03",
|
|
1900
|
+
"original": "帮我收着。明天有人来拿。",
|
|
1901
|
+
"issue": "无:两个短句,交代托付又不解释",
|
|
1902
|
+
"revised": "帮我收着。明天有人来拿。",
|
|
1903
|
+
"reason": "",
|
|
1904
|
+
"note": "压着嗓子,你不是请求,是把麻烦塞给她"
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
"scene": 1,
|
|
1908
|
+
"beat": 3,
|
|
1909
|
+
"speaker": "C01",
|
|
1910
|
+
"original": "你是谁?我们不代收——",
|
|
1911
|
+
"issue": "无:被打断的半句,正好是人已走远",
|
|
1912
|
+
"revised": "你是谁?我们不代收——",
|
|
1913
|
+
"reason": "",
|
|
1914
|
+
"note": "追问,话到一半发现对方根本不听"
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
"scene": 2,
|
|
1918
|
+
"beat": 3,
|
|
1919
|
+
"speaker": "C02",
|
|
1920
|
+
"original": "又是你值夜班。",
|
|
1921
|
+
"issue": "无:熟人寒暄,带着她天天值夜的信息",
|
|
1922
|
+
"revised": "又是你值夜班。",
|
|
1923
|
+
"reason": "",
|
|
1924
|
+
"note": "先吃一口再说,你在装作若无其事"
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
"scene": 2,
|
|
1928
|
+
"beat": 4,
|
|
1929
|
+
"speaker": "C01",
|
|
1930
|
+
"original": "陈叔,你怎么天天半夜来。",
|
|
1931
|
+
"issue": "无:顺口一问,眼睛在袋子上,潜台词在动作里",
|
|
1932
|
+
"revised": "陈叔,你怎么天天半夜来。",
|
|
1933
|
+
"reason": "",
|
|
1934
|
+
"note": "嘴上问他,心思全在袋子上,别看他"
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
"scene": 2,
|
|
1938
|
+
"beat": 5,
|
|
1939
|
+
"speaker": "C02",
|
|
1940
|
+
"original": "老了,睡不着。",
|
|
1941
|
+
"issue": "无:一句带过,把真正的原因藏住",
|
|
1942
|
+
"revised": "老了,睡不着。",
|
|
1943
|
+
"reason": "",
|
|
1944
|
+
"note": "笑着挡回去,不让她再问"
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
"scene": 2,
|
|
1948
|
+
"beat": 7,
|
|
1949
|
+
"speaker": "C02",
|
|
1950
|
+
"original": "这是什么?",
|
|
1951
|
+
"issue": "无:最短的问题,压力在筷子停住",
|
|
1952
|
+
"revised": "这是什么?",
|
|
1953
|
+
"reason": "",
|
|
1954
|
+
"note": "声音不变,你在试探,不是好奇"
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
"scene": 2,
|
|
1958
|
+
"beat": 8,
|
|
1959
|
+
"speaker": "C01",
|
|
1960
|
+
"original": "刚才一个人放这儿的,说明天有人来拿。",
|
|
1961
|
+
"issue": "无:耸肩一句,转述托付人的话,不加解释",
|
|
1962
|
+
"revised": "刚才一个人放这儿的,说明天有人来拿。",
|
|
1963
|
+
"reason": "",
|
|
1964
|
+
"note": "耸肩,你在敷衍,想把话题带过"
|
|
1965
|
+
},
|
|
1966
|
+
{
|
|
1967
|
+
"scene": 2,
|
|
1968
|
+
"beat": 10,
|
|
1969
|
+
"speaker": "C01",
|
|
1970
|
+
"original": "别人的东西。",
|
|
1971
|
+
"issue": "无:三个字划界线",
|
|
1972
|
+
"revised": "别人的东西。",
|
|
1973
|
+
"reason": "",
|
|
1974
|
+
"note": "手按住抽屉,你在拒绝,不用抬高声音"
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
"scene": 2,
|
|
1978
|
+
"beat": 11,
|
|
1979
|
+
"speaker": "C02",
|
|
1980
|
+
"original": "小夏,那个人穿雨衣?",
|
|
1981
|
+
"issue": "无:叫名字一次,用在试探的关键处",
|
|
1982
|
+
"revised": "小夏,那个人穿雨衣?",
|
|
1983
|
+
"reason": "",
|
|
1984
|
+
"note": "很轻,你在确认一件你早就知道的事"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
"scene": 2,
|
|
1988
|
+
"beat": 13,
|
|
1989
|
+
"speaker": "C02",
|
|
1990
|
+
"original": "三年前,这店里丢过一样东西。",
|
|
1991
|
+
"issue": "无:只给半句往事,留钩子",
|
|
1992
|
+
"revised": "三年前,这店里丢过一样东西。",
|
|
1993
|
+
"reason": "",
|
|
1994
|
+
"note": "背对她说,你在决定要说多少"
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
"scene": 2,
|
|
1998
|
+
"beat": 15,
|
|
1999
|
+
"speaker": "C02",
|
|
2000
|
+
"original": "别把它交给任何人。",
|
|
2001
|
+
"issue": "无:命令句,第一次正眼看她",
|
|
2002
|
+
"revised": "别把它交给任何人。",
|
|
2003
|
+
"reason": "",
|
|
2004
|
+
"note": "一字一顿,你在把责任压到她身上"
|
|
2005
|
+
},
|
|
2006
|
+
{
|
|
2007
|
+
"scene": 2,
|
|
2008
|
+
"beat": 17,
|
|
2009
|
+
"speaker": "VO",
|
|
2010
|
+
"original": "他怎么知道那人穿雨衣。",
|
|
2011
|
+
"issue": "无:心声一句,把怀疑交给观众",
|
|
2012
|
+
"revised": "他怎么知道那人穿雨衣。",
|
|
2013
|
+
"reason": "",
|
|
2014
|
+
"note": "压得很低,像自己不敢承认这个念头"
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
"scene": 2,
|
|
2018
|
+
"beat": 19,
|
|
2019
|
+
"speaker": "C02",
|
|
2020
|
+
"original": "锁上门。",
|
|
2021
|
+
"issue": "无:三个字收尾,命令里有恐惧",
|
|
2022
|
+
"revised": "锁上门。",
|
|
2023
|
+
"reason": "",
|
|
2024
|
+
"note": "不回头,你在逼她立刻照做"
|
|
2025
|
+
}
|
|
2026
|
+
],
|
|
2027
|
+
"selfCheck": [
|
|
2028
|
+
{
|
|
2029
|
+
"item": "读出声:有没有播音腔的句子?",
|
|
2030
|
+
"ok": true,
|
|
2031
|
+
"note": "全部短句口语,无书面长句"
|
|
2032
|
+
},
|
|
2033
|
+
{
|
|
2034
|
+
"item": "语气词密度:一句最多一个,没有粤语残留(啦/喇/啫)",
|
|
2035
|
+
"ok": true,
|
|
2036
|
+
"note": "几乎不用语气词"
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
"item": "网络用语符合年龄:长辈角色不用",
|
|
2040
|
+
"ok": true,
|
|
2041
|
+
"note": "老陈六十多岁,无网络用语"
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
"item": "地域口音只给少数角色",
|
|
2045
|
+
"ok": true,
|
|
2046
|
+
"note": "全员标准普通话"
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
"item": "英语夹杂符合行业身份",
|
|
2050
|
+
"ok": true,
|
|
2051
|
+
"note": "无英语夹杂,便利店夜班不需要"
|
|
2052
|
+
}
|
|
2053
|
+
]
|
|
1882
2054
|
}
|
|
1883
|
-
}
|
|
2055
|
+
}
|