@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.
- package/README.md +6 -1
- package/dist/index.js +109 -32
- package/package.json +1 -1
- package/skill/backreel/SKILL.md +29 -15
- package/skill/backreel/reference/crowdfunding.md +61 -30
- package/skill/backreel/reference/episodes.md +7 -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
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ref.mjs — 對白語料庫查詢/驗證工具(香港粵語及其他語體)
|
|
3
|
+
// 用法:node scripts/ref.mjs <command> [flags]
|
|
4
|
+
// 文件:references/data/README.md
|
|
5
|
+
|
|
6
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join, basename, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const DATA = join(ROOT, 'references', 'data');
|
|
12
|
+
|
|
13
|
+
const ENUMS = {
|
|
14
|
+
age: ['kid', 'teen', 'young', 'mid', 'senior', 'elder'],
|
|
15
|
+
archetype: ['student', 'slacker', 'office', 'boss', 'client', 'craftsman', 'waitress',
|
|
16
|
+
'shopkeeper', 'security', 'housewife', 'designer', 'street', 'elder', 'trainer',
|
|
17
|
+
'mum', 'dad', 'kid', 'teacher', 'sales', 'delivery'],
|
|
18
|
+
tone: ['sassy', 'blunt', 'cold', 'warm', 'complain', 'threat', 'whiny', 'helpless',
|
|
19
|
+
'gossip', 'funny', 'polite', 'angry', 'sarcastic', 'worried', 'proud', 'shy', 'flirty', 'surprised'],
|
|
20
|
+
function: ['greet', 'farewell', 'praise', 'insult', 'refuse', 'question', 'urge', 'comfort',
|
|
21
|
+
'bargain', 'gossip', 'joke', 'warn', 'threaten', 'dismiss', 'agree', 'disagree',
|
|
22
|
+
'complain', 'tease', 'flirt', 'apology', 'thanks', 'challenge', 'boast', 'surprise',
|
|
23
|
+
'encourage', 'refuse-soft'],
|
|
24
|
+
register: ['everyday', 'slang', 'internet', 'proverb', 'idiom', 'professional', 'english-mix', 'profanity'],
|
|
25
|
+
socialDistance: ['family', 'friend', 'colleague', 'client', 'stranger', 'conflict'],
|
|
26
|
+
};
|
|
27
|
+
const AGE_LABEL = { kid: '6-12', teen: '13-19', young: '20-29', mid: '30-44', senior: '45-59', elder: '60+' };
|
|
28
|
+
|
|
29
|
+
const LANG_LABEL = { 'zh-hk': '香港粵語', 'zh-cn': '大陸普通話', 'zh-tw': '台灣國語', 'zh-hk-mandarin': '港式普通話', 'zh-period': '古裝/年代戲普通話', en: '美式英文', ko: '韓文', ja: '日文' };
|
|
30
|
+
function langLabel(lang) { return LANG_LABEL[lang] || lang; }
|
|
31
|
+
|
|
32
|
+
// 自由標籤(跨維度)。新增標籤可以自由加,但最好保持呢個受控詞表,方便檢索。
|
|
33
|
+
const TAGS = [
|
|
34
|
+
// 場景/領域(粵語用前 10 個;其他語體用後半)
|
|
35
|
+
'裝修', '地盤', '茶記', '街市', '飲茶', '公司', '學校', '家庭', '大廈', '店舖',
|
|
36
|
+
'工地', '餐廳', '菜市場', '茶樓', '小區', '社區',
|
|
37
|
+
// 角色
|
|
38
|
+
'師傅', '師奶', '長輩', '後生', '老細', '客戶', '伙記', '管理員', '學生', 'OL', '外賣',
|
|
39
|
+
'大媽', '阿姨', '大叔', '老闆', '老闆娘', '服務員', '店員', '保安', '警衛',
|
|
40
|
+
'外賣員', '外送員', '司機', '白領', '上班族', '老師', '年輕人',
|
|
41
|
+
// 對白用途
|
|
42
|
+
'開場', '收尾', '拒絕', '讚', '踩', '八卦', '講價', '關心', '抱怨', '客套', '日常', '面試', '投訴',
|
|
43
|
+
// 語感
|
|
44
|
+
'直球', '冷面', '惜字如金', '潮語', '老派', '中英夾雜', '網絡用語',
|
|
45
|
+
// 圳景/角色補充(en/ko/ja 用)
|
|
46
|
+
'便利店', '地鐵', '居酒屋', '街頭', '酒吧', '咖啡廳', '校園', '職場',
|
|
47
|
+
'情侶', '上司', '下屬', '同事', '敬語', '半語', '方言', '黑話', '戀愛', '動漫', '遊戲', '外來語',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const flags = {};
|
|
52
|
+
const pos = [];
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const a = argv[i];
|
|
55
|
+
if (a === '-n') { flags.limit = argv[++i]; continue; }
|
|
56
|
+
if (a.startsWith('--')) {
|
|
57
|
+
const key = a.slice(2);
|
|
58
|
+
const next = argv[i + 1];
|
|
59
|
+
if (next === undefined || next.startsWith('--') || next === '-n') flags[key] = true;
|
|
60
|
+
else { flags[key] = next; i++; }
|
|
61
|
+
} else pos.push(a);
|
|
62
|
+
}
|
|
63
|
+
return { flags, pos };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function langDirs() {
|
|
67
|
+
if (!existsSync(DATA)) return [];
|
|
68
|
+
return readdirSync(DATA, { withFileTypes: true }).filter(function (d) { return d.isDirectory(); })
|
|
69
|
+
.map(function (d) { return d.name; }).sort();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readJson(file) {
|
|
73
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); }
|
|
74
|
+
catch (e) { throw new Error('JSON 解析失敗:' + file + ' — ' + e.message); }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shardFiles(lang) {
|
|
78
|
+
const dir = join(DATA, lang);
|
|
79
|
+
if (!existsSync(dir)) throw new Error('冇呢個語言目錄:references/data/' + lang + '(可用:' + (langDirs().join(', ') || '未有') + ')');
|
|
80
|
+
return readdirSync(dir).filter(function (f) { return f.endsWith('.json') && !f.endsWith('.all.json'); }).sort()
|
|
81
|
+
.map(function (f) { return join(dir, f); });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadLang(lang) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
const files = [];
|
|
87
|
+
const list = shardFiles(lang);
|
|
88
|
+
for (let i = 0; i < list.length; i++) {
|
|
89
|
+
const doc = readJson(list[i]);
|
|
90
|
+
if (!doc || !Array.isArray(doc.entries)) throw new Error(list[i] + ':缺少 entries 陣列');
|
|
91
|
+
files.push({ file: basename(list[i]), count: doc.entries.length });
|
|
92
|
+
for (let j = 0; j < doc.entries.length; j++) {
|
|
93
|
+
const e = doc.entries[j];
|
|
94
|
+
e._shard = basename(list[i], '.json');
|
|
95
|
+
entries.push(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { entries: entries, files: files };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function w(str) {
|
|
102
|
+
let n = 0;
|
|
103
|
+
const s = String(str === undefined || str === null ? '' : str);
|
|
104
|
+
for (const ch of s) n += /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/.test(ch) ? 2 : 1;
|
|
105
|
+
return n;
|
|
106
|
+
}
|
|
107
|
+
function pad(str, n) { const s = String(str === undefined || str === null ? '' : str); return s + ' '.repeat(Math.max(0, n - w(s))); }
|
|
108
|
+
function clip(str, n) {
|
|
109
|
+
const s = String(str === undefined || str === null ? '' : str);
|
|
110
|
+
if (w(s) <= n) return s;
|
|
111
|
+
let out = '', c = 0;
|
|
112
|
+
for (const ch of s) { const d = w(ch); if (c + d > n - 1) break; out += ch; c += d; }
|
|
113
|
+
return out + '…';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function table(rows, headers) {
|
|
117
|
+
const widths = headers.map(function (h, i) {
|
|
118
|
+
return Math.max.apply(null, [w(h)].concat(rows.map(function (r) { return w(r[i]); })));
|
|
119
|
+
});
|
|
120
|
+
function line(cells) { return cells.map(function (c, i) { return pad(c, widths[i]); }).join(' '); }
|
|
121
|
+
const head = line(headers);
|
|
122
|
+
const sep = line(widths.map(function (x) { return '─'.repeat(x); }));
|
|
123
|
+
return [head, sep].concat(rows.map(line)).join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function csv(v) {
|
|
127
|
+
if (v === undefined || v === true) return null;
|
|
128
|
+
const parts = String(v).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
|
129
|
+
return parts.length ? parts : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function test(allowed, actual) {
|
|
133
|
+
if (!allowed) return true;
|
|
134
|
+
if (!actual || !actual.length) return false;
|
|
135
|
+
return actual.some(function (a) { return allowed.indexOf(a) !== -1; });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function matches(entry, f) {
|
|
139
|
+
const c = entry.categories || {};
|
|
140
|
+
if (!test(csv(f.age), c.age)) return false;
|
|
141
|
+
if (!test(csv(f.archetype), c.archetype)) return false;
|
|
142
|
+
if (!test(csv(f.tone), c.tone)) return false;
|
|
143
|
+
if (!test(csv(f.function), c.function)) return false;
|
|
144
|
+
if (!test(csv(f.sd), c.socialDistance)) return false;
|
|
145
|
+
const entryTags = entry.tags || [];
|
|
146
|
+
const wantTags = csv(f.tag);
|
|
147
|
+
if (wantTags && !wantTags.every(function (t) { return entryTags.indexOf(t) !== -1; })) return false;
|
|
148
|
+
const anyTags = csv(f['any-tag']);
|
|
149
|
+
if (anyTags && !anyTags.some(function (t) { return entryTags.indexOf(t) !== -1; })) return false;
|
|
150
|
+
if (f.shard && f.shard !== true && entry._shard !== String(f.shard)) return false;
|
|
151
|
+
if (f.register && f.register !== true && (c.register || '') !== String(f.register)) return false;
|
|
152
|
+
const maxProf = f['max-prof'] === undefined ? Infinity : Number(f['max-prof']);
|
|
153
|
+
if (!Number.isNaN(maxProf) && (c.profanity || 0) > maxProf) return false;
|
|
154
|
+
const q = f.q || (f.search && f.search !== true ? f.search : null);
|
|
155
|
+
if (q) {
|
|
156
|
+
const hay = [entry.phrase].concat(entry.variants || [], [entry.meaning, entry.literal, entry.example, entry.situation, entry.note])
|
|
157
|
+
.filter(Boolean).join(' ').toLowerCase();
|
|
158
|
+
if (hay.indexOf(String(q).toLowerCase()) === -1) return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const REQUIRED = ['id', 'phrase', 'meaning', 'example', 'situation'];
|
|
164
|
+
function validateEntries(entries) {
|
|
165
|
+
const errors = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
const ids = {};
|
|
168
|
+
const phrases = {};
|
|
169
|
+
|
|
170
|
+
for (const e of entries) {
|
|
171
|
+
const where = e._shard + ':' + (e.id || '?');
|
|
172
|
+
for (const k of REQUIRED) if (!e[k] || typeof e[k] !== 'string') errors.push(where + ' 缺少/非字串欄位 ' + k);
|
|
173
|
+
if (e.id && !/^[a-z0-9-]+$/.test(e.id)) errors.push(where + ' id 只准細楷英數及連字符');
|
|
174
|
+
if (e.id) {
|
|
175
|
+
if (ids[e.id]) errors.push('重複 id:' + e.id + '(' + ids[e.id] + ' 及 ' + e._shard + ')');
|
|
176
|
+
else ids[e.id] = e._shard;
|
|
177
|
+
}
|
|
178
|
+
const p = (e.phrase || '').trim();
|
|
179
|
+
if (p) {
|
|
180
|
+
const k = p.toLowerCase();
|
|
181
|
+
if (phrases[k]) warnings.push('重複 phrase:' + p + '(' + phrases[k] + ' 及 ' + e._shard + ')');
|
|
182
|
+
else phrases[k] = e._shard;
|
|
183
|
+
}
|
|
184
|
+
const c = e.categories;
|
|
185
|
+
if (!c || typeof c !== 'object') { errors.push(where + ' 缺少 categories'); continue; }
|
|
186
|
+
['tone', 'function'].forEach(function (key) {
|
|
187
|
+
if (!Array.isArray(c[key]) || c[key].length === 0) errors.push(where + ' categories.' + key + ' 必須是非空陣列');
|
|
188
|
+
});
|
|
189
|
+
['age', 'archetype', 'tone', 'function', 'socialDistance'].forEach(function (key) {
|
|
190
|
+
if (c[key] === undefined) return;
|
|
191
|
+
if (!Array.isArray(c[key])) { errors.push(where + ' categories.' + key + ' 必須是陣列'); return; }
|
|
192
|
+
c[key].forEach(function (v) {
|
|
193
|
+
if (ENUMS[key].indexOf(v) === -1) errors.push(where + ' categories.' + key + ' 有未知值「' + v + '」');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
if (ENUMS.register.indexOf(c.register) === -1) errors.push(where + ' categories.register 無效:' + c.register);
|
|
197
|
+
if (e.tags !== undefined) {
|
|
198
|
+
if (!Array.isArray(e.tags)) errors.push(where + ' tags 必須是字串陣列');
|
|
199
|
+
else {
|
|
200
|
+
const seen = {};
|
|
201
|
+
for (const t of e.tags) {
|
|
202
|
+
if (typeof t !== 'string' || !t.trim()) errors.push(where + ' tags 有空白或非字串值');
|
|
203
|
+
else if (seen[t]) errors.push(where + ' tags 重複:' + t);
|
|
204
|
+
else { seen[t] = true; if (TAGS.indexOf(t) === -1) warnings.push(where + ' 未知 tag「' + t + '」(唔在受控詞表,可接受但要一致)'); }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (typeof c.profanity !== 'number' || !Number.isInteger(c.profanity) || c.profanity < 0 || c.profanity > 3) {
|
|
209
|
+
errors.push(where + ' categories.profanity 必須是 0-3 整數');
|
|
210
|
+
}
|
|
211
|
+
if (c.profanity >= 2 && c.register !== 'profanity' && c.register !== 'slang') {
|
|
212
|
+
warnings.push(where + ' profanity>=2 但 register=' + c.register);
|
|
213
|
+
}
|
|
214
|
+
if (c.profanity >= 2 && !(e.avoid && e.avoid.length)) warnings.push(where + ' profanity>=2 但冇寫 avoid');
|
|
215
|
+
if (e.example && e.phrase && e.example.indexOf(e.phrase.slice(0, 2)) === -1 && !(e.variants || []).some(function (v) { return e.example.indexOf(v.slice(0, 2)) !== -1; })) {
|
|
216
|
+
warnings.push(where + ' example 同 phrase 關聯弱');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { entries: entries, errors: errors, warnings: warnings };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function validateLang(lang) {
|
|
223
|
+
return validateEntries(loadLang(lang).entries);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function cmdCheck(file) {
|
|
227
|
+
const doc = readJson(file);
|
|
228
|
+
if (!doc || !Array.isArray(doc.entries)) throw new Error(file + ': 缺少 entries 陣列');
|
|
229
|
+
const shard = basename(file, '.json');
|
|
230
|
+
const entries = doc.entries.map(function (e) { e._shard = shard; return e; });
|
|
231
|
+
const res = validateEntries(entries);
|
|
232
|
+
console.log('檢查 ' + file + ':' + res.entries.length + ' 條 錯誤 ' + res.errors.length + ' 警告 ' + res.warnings.length);
|
|
233
|
+
res.errors.forEach(function (e) { console.log(' ✗ ' + e); });
|
|
234
|
+
res.warnings.forEach(function (e) { console.log(' ! ' + e); });
|
|
235
|
+
process.exitCode = res.errors.length ? 1 : 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function row(e) {
|
|
239
|
+
const c = e.categories || {};
|
|
240
|
+
return [e.id, e.phrase, clip(e.meaning, 34), clip((e.tags || []).join('/'), 16), (c.tone || []).join('/'), c.register, 'p' + c.profanity];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function printEntries(list, json) {
|
|
244
|
+
if (json) {
|
|
245
|
+
console.log(JSON.stringify(list.map(function (e) {
|
|
246
|
+
const o = JSON.parse(JSON.stringify(e)); delete o._shard; return o;
|
|
247
|
+
}), null, 2));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!list.length) { console.log('(冇結果)'); return; }
|
|
251
|
+
console.log(table(list.map(row), ['id', 'phrase', 'meaning', '標籤', 'tone', 'reg', 'prof']));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function printFull(e) {
|
|
255
|
+
const c = e.categories || {};
|
|
256
|
+
const lines = [];
|
|
257
|
+
lines.push('● ' + e.phrase + (e.variants && e.variants.length ? ' (亦作:' + e.variants.join('、') + ')' : '') + ' [' + e.id + ']');
|
|
258
|
+
lines.push(' 意思:' + e.meaning);
|
|
259
|
+
if (e.literal) lines.push(' 字面:' + e.literal);
|
|
260
|
+
if (e.jyutping) lines.push(' 粵拼:' + e.jyutping);
|
|
261
|
+
if (e.romanization) lines.push(' 羅馬字:' + e.romanization);
|
|
262
|
+
lines.push(' 年齡:' + ((c.age || []).map(function (a) { return a + '(' + AGE_LABEL[a] + ')'; }).join('、') || '任何'));
|
|
263
|
+
lines.push(' 角色:' + ((c.archetype || []).join('、') || '任何'));
|
|
264
|
+
lines.push(' 語氣:' + ((c.tone || []).join('、')));
|
|
265
|
+
lines.push(' 功能:' + ((c.function || []).join('、')));
|
|
266
|
+
lines.push(' 語體:' + c.register + '/粗口級別 ' + c.profanity);
|
|
267
|
+
if (c.socialDistance) lines.push(' 對象:' + c.socialDistance.join('、'));
|
|
268
|
+
if (e.tags && e.tags.length) lines.push(' 標籤:' + e.tags.join('、'));
|
|
269
|
+
lines.push(' 例:' + e.example);
|
|
270
|
+
lines.push(' 場口:' + (e.situation || ''));
|
|
271
|
+
if (e.avoid && e.avoid.length) lines.push(' 小心:' + e.avoid.join(';'));
|
|
272
|
+
if (e.note) lines.push(' 註:' + e.note);
|
|
273
|
+
if (e.source && e.source !== 'general') lines.push(' 來源:' + e.source);
|
|
274
|
+
console.log(lines.join('\n'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function cmdStats(lang) {
|
|
278
|
+
const data = loadLang(lang);
|
|
279
|
+
const entries = data.entries;
|
|
280
|
+
console.log('語言:' + lang + ' 總數:' + entries.length + ' 條');
|
|
281
|
+
console.log('shards:' + data.files.map(function (f) { return f.file + '(' + f.count + ')'; }).join(', '));
|
|
282
|
+
const keys = ['age', 'archetype', 'tone', 'function', 'register', 'profanity'];
|
|
283
|
+
for (const key of keys) {
|
|
284
|
+
const m = {};
|
|
285
|
+
for (const e of entries) {
|
|
286
|
+
const c = e.categories || {};
|
|
287
|
+
const vals = key === 'register' ? [c.register] : key === 'profanity' ? ['p' + c.profanity] : (c[key] || ['(未標)']);
|
|
288
|
+
for (const v of vals) m[v] = (m[v] || 0) + 1;
|
|
289
|
+
}
|
|
290
|
+
const parts = Object.keys(m).sort(function (a, b) { return m[b] - m[a]; }).map(function (k) { return k + '=' + m[k]; });
|
|
291
|
+
console.log('\n[' + key + '] ' + parts.join(' '));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function cmdMarkdown(lang, by) {
|
|
296
|
+
const data = loadLang(lang);
|
|
297
|
+
const entries = data.entries;
|
|
298
|
+
const key = by && by !== true ? String(by) : 'age';
|
|
299
|
+
const order = ENUMS[key] || [];
|
|
300
|
+
const out = [];
|
|
301
|
+
for (const v of order) {
|
|
302
|
+
const list = entries.filter(function (e) { return (e.categories[key] || []).indexOf(v) !== -1; });
|
|
303
|
+
if (!list.length) continue;
|
|
304
|
+
out.push('#### ' + v + '(' + list.length + ')');
|
|
305
|
+
out.push('');
|
|
306
|
+
out.push('| 講法 | 意思 | 語氣 | 例子 |');
|
|
307
|
+
out.push('|---|---|---|---|');
|
|
308
|
+
for (const e of list) {
|
|
309
|
+
const c = e.categories;
|
|
310
|
+
const label = e.phrase + (c.profanity ? ' (p' + c.profanity + ')' : '');
|
|
311
|
+
out.push('| ' + label + ' | ' + (e.meaning || '').replace(/\|/g, '\\|') + ' | ' + (c.tone || []).join('/') + ' | ' + (e.example || '').replace(/\|/g, '\\|') + ' |');
|
|
312
|
+
}
|
|
313
|
+
out.push('');
|
|
314
|
+
}
|
|
315
|
+
console.log(out.join('\n'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cmdIndex(lang) {
|
|
319
|
+
const entries = loadLang(lang).entries;
|
|
320
|
+
const ALL_AGES = ENUMS.age.slice();
|
|
321
|
+
const isUniversal = function (e) {
|
|
322
|
+
const c = e.categories || {};
|
|
323
|
+
const ages = c.age || [];
|
|
324
|
+
return ages.length >= 5 && !(c.archetype && c.archetype.length);
|
|
325
|
+
};
|
|
326
|
+
function tbl(list) {
|
|
327
|
+
const rows = list.map(function (e) {
|
|
328
|
+
const c = e.categories || {};
|
|
329
|
+
const phrase = e.phrase + (c.profanity ? " (p" + c.profanity + ")" : "");
|
|
330
|
+
const tg = (e.tags || []).slice(0, 3).join("/");
|
|
331
|
+
return "| " + phrase + " | " + (e.meaning || "").replace(/\|/g, "\\|") + " | " + (e.example || "").replace(/\|/g, "\\|") + " | " + tg + " |";
|
|
332
|
+
});
|
|
333
|
+
return ["| 講法 | 意思 | 例子 | 標籤 |", "|---|---|---|---|"].concat(rows).join("\n");
|
|
334
|
+
}
|
|
335
|
+
const out = [];
|
|
336
|
+
out.push("# " + langLabel(lang) + "語料庫索引(自動生成,唔好手改)");
|
|
337
|
+
out.push("");
|
|
338
|
+
out.push("生成:node scripts/ref.mjs index > references/zh-hk-index.md");
|
|
339
|
+
out.push("");
|
|
340
|
+
out.push("查詢:" + String.fromCharCode(96) + "node scripts/ref.mjs list/search/show" + String.fromCharCode(96) + ";schema:" + String.fromCharCode(96) + "references/data/README.md" + String.fromCharCode(96));
|
|
341
|
+
out.push("");
|
|
342
|
+
|
|
343
|
+
const universal = entries.filter(isUniversal);
|
|
344
|
+
out.push("## 一、核心萬用(跨年齡、跨角色) " + universal.length + " 條");
|
|
345
|
+
out.push("");
|
|
346
|
+
out.push(tbl(universal));
|
|
347
|
+
out.push("");
|
|
348
|
+
|
|
349
|
+
out.push("## 二、按年齡");
|
|
350
|
+
out.push("");
|
|
351
|
+
for (const age of ENUMS.age) {
|
|
352
|
+
const list = entries.filter(function (e) { return (e.categories.age || []).indexOf(age) !== -1 && !isUniversal(e); });
|
|
353
|
+
if (!list.length) continue;
|
|
354
|
+
out.push("### " + age + "(" + AGE_LABEL[age] + ") " + list.length + " 條");
|
|
355
|
+
out.push("");
|
|
356
|
+
out.push(tbl(list));
|
|
357
|
+
out.push("");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
out.push("## 三、按角色類型");
|
|
361
|
+
out.push("");
|
|
362
|
+
for (const arch of ENUMS.archetype) {
|
|
363
|
+
const list = entries.filter(function (e) { return (e.categories.archetype || []).indexOf(arch) !== -1; });
|
|
364
|
+
if (!list.length) continue;
|
|
365
|
+
out.push("### " + arch + " " + list.length + " 條");
|
|
366
|
+
out.push("");
|
|
367
|
+
out.push(tbl(list));
|
|
368
|
+
out.push("");
|
|
369
|
+
}
|
|
370
|
+
console.log(out.join("\n"));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function csvCell(v) {
|
|
374
|
+
const t = String(v === undefined || v === null ? '' : v);
|
|
375
|
+
return /[",\n]/.test(t) ? '"' + t.replace(/"/g, '""') + '"' : t;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function cmdSentences(lang, json, csv) {
|
|
379
|
+
const entries = loadLang(lang).entries;
|
|
380
|
+
if (csv) {
|
|
381
|
+
const rows = [['id', 'shard', 'sentence', 'phrase', 'meaning', 'tags', 'register', 'profanity', 'tone', 'function']];
|
|
382
|
+
entries.forEach(function (e) {
|
|
383
|
+
const c = e.categories || {};
|
|
384
|
+
rows.push([e.id, e._shard, e.example, e.phrase, e.meaning, (e.tags || []).join('/'),
|
|
385
|
+
c.register, c.profanity, (c.tone || []).join('/'), (c.function || []).join('/')]);
|
|
386
|
+
});
|
|
387
|
+
console.log('\uFEFF' + rows.map(function (r) { return r.map(csvCell).join(','); }).join('\n'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (json) {
|
|
391
|
+
console.log(JSON.stringify(entries.map(function (e) {
|
|
392
|
+
const c = e.categories || {};
|
|
393
|
+
return { id: e.id, shard: e._shard, phrase: e.phrase, sentence: e.example, meaning: e.meaning,
|
|
394
|
+
tone: c.tone, function: c.function, register: c.register, profanity: c.profanity, tags: e.tags || [] };
|
|
395
|
+
}), null, 2));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const order = ["core", "rough", "older", "workplace", "gen-z"];
|
|
399
|
+
const shards = {};
|
|
400
|
+
entries.forEach(function (e) { (shards[e._shard] = shards[e._shard] || []).push(e); });
|
|
401
|
+
const out = [];
|
|
402
|
+
out.push("# " + langLabel(lang) + "對白句庫(全部 " + entries.length + " 句,自動生成)");
|
|
403
|
+
out.push("");
|
|
404
|
+
out.push("每句=語料庫一條 entry 嘅 example。格式:句子 | 講法 | 意思 | 標籤");
|
|
405
|
+
out.push("");
|
|
406
|
+
const names = Object.keys(shards).sort(function (a, b) {
|
|
407
|
+
const ia = order.indexOf(a), ib = order.indexOf(b);
|
|
408
|
+
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
|
|
409
|
+
});
|
|
410
|
+
names.forEach(function (name) {
|
|
411
|
+
const list = shards[name].slice().sort(function (a, b) { return a.id.localeCompare(b.id); });
|
|
412
|
+
out.push("## " + name + " " + list.length + " 句");
|
|
413
|
+
out.push("");
|
|
414
|
+
list.forEach(function (e) {
|
|
415
|
+
const c = e.categories || {};
|
|
416
|
+
out.push("- " + e.example + " | " + e.phrase + " | " + e.meaning + (e.tags && e.tags.length ? " | " + e.tags.join("/") : ""));
|
|
417
|
+
});
|
|
418
|
+
out.push("");
|
|
419
|
+
});
|
|
420
|
+
console.log(out.join("\n"));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function cmdBuild(lang) {
|
|
424
|
+
const data = loadLang(lang);
|
|
425
|
+
const sorted = data.entries.map(function (e) {
|
|
426
|
+
const o = JSON.parse(JSON.stringify(e)); delete o._shard; return o;
|
|
427
|
+
}).sort(function (a, b) { return a.id.localeCompare(b.id); });
|
|
428
|
+
const doc = { lang: lang, count: sorted.length, generatedAt: new Date().toISOString().slice(0, 10), enums: ENUMS, entries: sorted };
|
|
429
|
+
const file = join(DATA, lang + '.all.json');
|
|
430
|
+
writeFileSync(file, JSON.stringify(doc, null, 2) + '\n');
|
|
431
|
+
console.log('已寫出 ' + file + '(' + sorted.length + ' 條)');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function main() {
|
|
435
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
436
|
+
const flags = parsed.flags;
|
|
437
|
+
const pos = parsed.pos;
|
|
438
|
+
const cmd = pos[0];
|
|
439
|
+
const lang = flags.lang && flags.lang !== true ? String(flags.lang) : 'zh-hk';
|
|
440
|
+
const json = Boolean(flags.json);
|
|
441
|
+
|
|
442
|
+
if (!cmd) {
|
|
443
|
+
console.log('用法:node scripts/ref.mjs <command> [--lang zh-hk]');
|
|
444
|
+
console.log(' list [--age teen] [--archetype office] [--tone sassy] [--function insult]');
|
|
445
|
+
console.log(' [--sd friend] [--register slang] [--tag 裝修,地盤] [--any-tag 踩,讚]');
|
|
446
|
+
console.log(' [--max-prof 0] [--q 字] [--limit 20] [--json]');
|
|
447
|
+
console.log(' search <字> 全文搜尋(講法/意思/例子)');
|
|
448
|
+
console.log(' show <id> 睇一條詳細');
|
|
449
|
+
console.log(' random [-n 5] 隨機抽(可加 filter)');
|
|
450
|
+
console.log(' stats 統計分佈');
|
|
451
|
+
console.log(' tags 列出所有可用標籤(--values 睇實際 tag 分佈)');
|
|
452
|
+
console.log(' validate 驗證整個語言語料庫');
|
|
453
|
+
console.log(' check <file> 驗證單一 shard(寫稿時用)');
|
|
454
|
+
console.log(' build 合併成 <lang>.all.json');
|
|
455
|
+
console.log(' index 生成人讀索引(可 redirect 成 .md)');
|
|
456
|
+
console.log(' sentences 列出所有對白句(--json/--csv;可 redirect 成檔案)');
|
|
457
|
+
console.log(' markdown --by age 生成 markdown 表(age/archetype/tone/function)');
|
|
458
|
+
console.log(' langs 列出所有語言語料庫');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (cmd === 'langs') {
|
|
463
|
+
const langs = langDirs();
|
|
464
|
+
if (!langs.length) { console.log('(未有語料庫)'); return; }
|
|
465
|
+
for (const l of langs) {
|
|
466
|
+
let n = 0;
|
|
467
|
+
try { n = loadLang(l).entries.length; } catch (e) { n = -1; }
|
|
468
|
+
console.log(l + '\t' + n + ' 條');
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (cmd === 'tags') {
|
|
473
|
+
if (flags.values) {
|
|
474
|
+
const all = loadLang(lang).entries;
|
|
475
|
+
const m = {};
|
|
476
|
+
all.forEach(function (e) { (e.tags || []).forEach(function (t) { m[t] = (m[t] || 0) + 1; }); });
|
|
477
|
+
const parts = Object.keys(m).sort(function (a, b) { return m[b] - m[a]; }).map(function (k) { return k + '=' + m[k]; });
|
|
478
|
+
console.log('實際 tag(' + all.length + ' 條):');
|
|
479
|
+
console.log(parts.join(' '));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
Object.keys(ENUMS).forEach(function (key) { console.log(pad(key, 15) + ENUMS[key].join(', ')); });
|
|
483
|
+
console.log(pad('age-label', 15) + Object.keys(AGE_LABEL).map(function (k) { return k + '=' + AGE_LABEL[k]; }).join(', '));
|
|
484
|
+
console.log(pad('tag-vocab', 15) + TAGS.join(', '));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (cmd === 'stats') return cmdStats(lang);
|
|
488
|
+
if (cmd === 'build') return cmdBuild(lang);
|
|
489
|
+
if (cmd === 'index') return cmdIndex(lang);
|
|
490
|
+
if (cmd === 'sentences') return cmdSentences(lang, json, Boolean(flags.csv));
|
|
491
|
+
if (cmd === 'markdown') return cmdMarkdown(lang, flags.by);
|
|
492
|
+
|
|
493
|
+
if (cmd === 'check') {
|
|
494
|
+
if (!pos[1]) throw new Error('用法:ref.mjs check references/data/zh-hk/xxx.json');
|
|
495
|
+
return cmdCheck(pos[1]);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (cmd === 'validate') {
|
|
499
|
+
const res = validateLang(lang);
|
|
500
|
+
console.log('驗證 ' + lang + ':' + res.entries.length + ' 條 錯誤 ' + res.errors.length + ' 警告 ' + res.warnings.length);
|
|
501
|
+
res.errors.forEach(function (e) { console.log(' ✗ ' + e); });
|
|
502
|
+
res.warnings.forEach(function (e) { console.log(' ! ' + e); });
|
|
503
|
+
process.exitCode = res.errors.length ? 1 : 0;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (cmd === 'show') {
|
|
508
|
+
if (!pos[1]) throw new Error('用法:ref.mjs show <id>');
|
|
509
|
+
const hits = loadLang(lang).entries.filter(function (e) { return e.id === pos[1]; });
|
|
510
|
+
if (!hits.length) { console.log('搵唔到 id:' + pos[1]); process.exitCode = 1; return; }
|
|
511
|
+
hits.forEach(printFull);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (cmd === 'search') {
|
|
516
|
+
if (!pos[1]) throw new Error('用法:ref.mjs search <字詞>');
|
|
517
|
+
const f = Object.assign({}, flags, { q: pos[1] });
|
|
518
|
+
printEntries(loadLang(lang).entries.filter(function (e) { return matches(e, f); }), json);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (cmd === 'random') {
|
|
523
|
+
const pool = loadLang(lang).entries.filter(function (e) { return matches(e, flags); });
|
|
524
|
+
const n = Number(flags.limit || 5);
|
|
525
|
+
const picked = pool.map(function (e) { return [Math.random(), e]; }).sort(function (a, b) { return a[0] - b[0]; })
|
|
526
|
+
.slice(0, n).map(function (p) { return p[1]; });
|
|
527
|
+
printEntries(picked, json);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (cmd !== 'list') throw new Error('未知指令:' + cmd + '(可用:list search show random stats tags validate build markdown langs)');
|
|
532
|
+
|
|
533
|
+
const hits = loadLang(lang).entries.filter(function (e) { return matches(e, flags); });
|
|
534
|
+
const limit = Number(flags.limit || 0);
|
|
535
|
+
printEntries(limit ? hits.slice(0, limit) : hits, json);
|
|
536
|
+
if (!json) console.log('\n(' + hits.length + ' 條' + (limit && hits.length > limit ? ',顯示首 ' + limit : '') + ')');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try { main(); } catch (e) { console.error('錯誤:' + e.message); process.exitCode = 1; }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 對白校對及修改報告
|
|
2
|
+
|
|
3
|
+
## 一、整體診斷
|
|
4
|
+
|
|
5
|
+
本段落概述此場對白的主要問題,包括場面目標、人物攻防、潛台詞、角色聲音、資訊處理、節奏與可拍攝性。若資料不足,應清楚標明假設。
|
|
6
|
+
|
|
7
|
+
| 項目 | 判斷 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 語體 | 粵語/港式・大陸普通話・台灣國語・港式普通話・古裝年代・英文(美式/英式) |
|
|
10
|
+
| 場面目標 | |
|
|
11
|
+
| 主要衝突 | |
|
|
12
|
+
| 潛台詞核心 | |
|
|
13
|
+
| 角色聲音問題 | |
|
|
14
|
+
| 節奏問題 | |
|
|
15
|
+
| 建議修改幅度 | 輕修/中修/重修 |
|
|
16
|
+
|
|
17
|
+
### 英文版本專用(非英文任務可刪除本表)
|
|
18
|
+
|
|
19
|
+
| 項目 | 判斷 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 目標地區 | 美式為主/英式為主/混合(註明哪些角色用英式) |
|
|
22
|
+
| 角色世代 | 逐角色列出出生年區間與 signature 用語 |
|
|
23
|
+
| 投放平台與尺度 | ReelShort/DramaBox/TikTok/串流;粗口上限 |
|
|
24
|
+
| Transcreation 幅度 | 貼近原文/功能等價重寫(可合併、拆分、改變攻防順序) |
|
|
25
|
+
| 主要翻譯腔病徵 | 呼名過密/不用 contraction/情緒宣言/敬語殘留/成語直譯/主語缺失 |
|
|
26
|
+
|
|
27
|
+
## 二、逐句校對
|
|
28
|
+
|
|
29
|
+
| 原句 | 問題 | 修改句 | 修改理由 | 導演/演員提示 |
|
|
30
|
+
|---|---|---|---|---|
|
|
31
|
+
| | | | | |
|
|
32
|
+
|
|
33
|
+
## 三、整場修改版
|
|
34
|
+
|
|
35
|
+
在此提供可直接放回劇本的修改版。應保留必要角色名、格式與場景標記;若未能確定格式,使用清晰的劇本對白格式呈現。
|
|
36
|
+
|
|
37
|
+
## 四、導演處理建議
|
|
38
|
+
|
|
39
|
+
在此補充場面節拍、沉默、視線、身體距離、走位、反應鏡頭與演員行動動詞。建議以可執行動作描述,而非抽象情緒形容。
|
|
40
|
+
|
|
41
|
+
## 五、後續可加強事項
|
|
42
|
+
|
|
43
|
+
如需要用戶補充角色背景、前後情節、地域語言、類型風格、審查尺度或希望保留的金句,在此列明。
|