From e6e859918a9006dad02acb54b86881c82ff646cc Mon Sep 17 00:00:00 2001 From: baseballyama Date: Sun, 27 Sep 2026 21:10:22 +0900 Subject: [PATCH] feat(skill): plan talk and document decks and review Japanese wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slides written with the skill read as machine-made in Japanese: stock phrases, dashes, 「効く」, label-colon bullets, and pages that contradict each other. The skill now separates talk decks (question dividers, no answer up front) from document decks (answer first, assertion headlines), adds Japanese wording rules, and a review step with a text/flag extractor and a context-free reader. Rules are adapted from coji/natural-japanese, minorun365's slide-story and writing guide, carnot-tech/consulting-pptx-skill and tyroneross/pyramid-principle; licenses are recorded in sources.md. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0116eREiYfHTFLy5Eu3nwh5f --- .../skills/consulting-deck-design/SKILL.md | 6 + README.md | 6 +- packages/dev/test/skill-deck-text.test.mjs | 62 ++++ skill/SKILL.md | 33 +- skill/references/review.md | 99 ++++++ skill/references/sources.md | 40 +++ skill/references/story-document.md | 145 +++++++++ skill/references/story-talk.md | 206 ++++++++++++ skill/references/tsx.md | 22 +- skill/references/writing-ja.md | 112 +++++++ skill/scripts/deck-text.mjs | 295 ++++++++++++++++++ 11 files changed, 1018 insertions(+), 8 deletions(-) create mode 100644 packages/dev/test/skill-deck-text.test.mjs create mode 100644 skill/references/review.md create mode 100644 skill/references/sources.md create mode 100644 skill/references/story-document.md create mode 100644 skill/references/story-talk.md create mode 100644 skill/references/writing-ja.md create mode 100644 skill/scripts/deck-text.mjs diff --git a/.claude/skills/consulting-deck-design/SKILL.md b/.claude/skills/consulting-deck-design/SKILL.md index 63648a04..00079991 100644 --- a/.claude/skills/consulting-deck-design/SKILL.md +++ b/.claude/skills/consulting-deck-design/SKILL.md @@ -398,6 +398,12 @@ several rounds, and each round's fix sometimes introduced a _new_ tell of a different flavor. Treat "sounding human" as an ongoing discipline applied while drafting, not a find-and-replace pass done at the end. +The general storyline, headline and Japanese wording rules live in the +distributable skill: `skill/references/story-document.md`, +`skill/references/writing-ja.md` and `skill/references/review.md` (run +`skill/scripts/deck-text.mjs --mode document` on the exported deck). This +section adds only what is specific to consulting-style exhibits. + ### Numeric self-consistency is the single strongest signal The #1 thing that got a deck flagged as AI-generated was a hard arithmetic diff --git a/README.md b/README.md index bf2fee25..eca6b118 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,11 @@ and a next-actions table. Use labelled sample data where needed. The [skill](skill/SKILL.md) creates a TSX project, starts the interactive preview, checks the source and exports an editable PPTX. Ask for changes in the same -conversation; the preview updates as the agent edits TSX. You need Node.js 22.18+ +conversation; the preview updates as the agent edits TSX. It plans the storyline for +either a talk or a deck people read and decide from, and checks the headlines and +Japanese wording before delivery ([talk](skill/references/story-talk.md), +[document](skill/references/story-document.md), [review](skill/references/review.md)). +You need Node.js 22.18+ and Git alongside Claude Code. See the [authoring guide](https://office-kit.github.io/pptx/docs/authoring) for template editing and manual setup. The bundled [core reference](skill/references/core-api.md) diff --git a/packages/dev/test/skill-deck-text.test.mjs b/packages/dev/test/skill-deck-text.test.mjs new file mode 100644 index 00000000..c695a3c6 --- /dev/null +++ b/packages/dev/test/skill-deck-text.test.mjs @@ -0,0 +1,62 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile, rm, symlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { buildDeck, initProject } from '../dist/index.mjs'; + +const execute = promisify(execFile); +const script = fileURLToPath(new URL('../../../skill/scripts/deck-text.mjs', import.meta.url)); +const devModules = fileURLToPath(new URL('../node_modules', import.meta.url)); + +// The skill tells agents to name headlines "Headline" and run this script from the +// slide project; this keeps the documented workflow working end to end. +test('the skill deck-text script reads named headlines and flags wording', async (t) => { + const directory = await mkdtemp(join(tmpdir(), 'pptx-deck-text-test-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const project = await initProject(join(directory, 'slides')); + await symlink(devModules, join(project, 'node_modules'), 'dir'); + const deck = join(project, 'deck.tsx'); + await writeFile( + deck, + `import { Presentation, Slide, Text } from '@office-kit/pptx-dsl'; +export default ( + + + 権限はどこで決まるの? + 1 + 行レベルの権限が効く + + + 目次 + 値は有効期間つきで積む + DB 制約で守り切る + — 前提 + + +); +`, + ); + const built = await buildDeck(deck); + await writeFile(join(project, 'deck.pptx'), built.bytes); + + const { stdout } = await execute(process.execPath, [script, 'deck.pptx', '--mode', 'talk'], { + cwd: project, + }); + assert.match(stdout, /^p01 権限はどこで決まるの?$/m); + assert.match(stdout, /^p02 目次$/m); + assert.match(stdout, /? 50%/); + assert.match(stdout, /^p01 \[効く\] 行レベルの権限が効く$/m); + assert.match(stdout, /^p02 \[dash\] — 前提$/m); + assert.match(stdout, /^p02 notes \[効く\] ここで RLS が効いてくる$/m); + assert.match(stdout, /^p02 \[intensifying 切る\] DB 制約で守り切る$/m); + assert.doesNotMatch(stdout, /有効期間/); + + await assert.rejects( + execute(process.execPath, [script, 'deck.pptx', '--mode', 'slides'], { cwd: project }), + /Usage/, + ); +}); diff --git a/skill/SKILL.md b/skill/SKILL.md index 69714b98..5299e6c4 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -37,6 +37,28 @@ existing directory; reuse a presentation project or choose a new directory. Read the generated `CLAUDE.md` before authoring. It is not necessarily loaded automatically when Claude was started in the parent directory. +## Plan the story and the words + +Decide which kind of deck this is before writing any slide text, including outline +drafts shown to the user: + +- **Talk** (the default when a speaker presents it: conference talk, lightning talk, + lecture, all-hands). Read [the talk rules](references/story-talk.md). +- **Document** (read without the author, or used to reach a decision: report, proposal, + decision memo, board pack, handout). Read [the document rules](references/story-document.md). + +If the brief does not make the kind clear, ask. For a Japanese deck, also read +[the Japanese wording rules](references/writing-ja.md). The two modes deliberately +disagree: a talk reveals its answer step by step and mixes headline forms; a document +states the answer first and makes every headline an assertion. + +For a new deck, gather the user's real material first and agree a one-line-per-slide +storyline before building. Never invent figures, customers, quotes or anecdotes; ask, +or leave a visible `[要確認: …]`. Give each slide's headline `Text` the prop +`name="Headline"` so the checks can find it. When revising an existing deck, change +wording only where the reader gains something concrete, keep the speaker's voice, and +change structure only when asked. + ## Author and revise Read [the TSX reference](references/tsx.md) when first authoring or using unfamiliar @@ -94,8 +116,10 @@ Saving TSX updates the preview. The user selects slides in the vertical thumbnai strip, uses Fit/zoom, or chooses Present for presentation mode. Review affected slides with available browser/image tools and fix problems in TSX. Keep this loop to source edits and the running preview; do not run a separate export or restart -the server for each intermediate change. If visual inspection -is unavailable, state that limitation rather than claiming a visual check. +the server for each intermediate change. Without browser tools, render the exported +deck to images (for example `soffice --headless --convert-to pdf deck.pptx` and +`pdftoppm -r 80 -png deck.pdf page`) and inspect every page. If no visual inspection is +possible, state that limitation rather than claiming a visual check. A failed build leaves the last successful preview visible with an error. Fix the error before treating the visible slides or Download PPTX as current output. @@ -111,7 +135,10 @@ npm run build ``` Type checking is separate from the live rebuild. Fix errors and review all slides, -including template slides retained in edit mode. The SVG preview is a rendering +including template slides retained in edit mode. Then review the words and the story +with [the review steps](references/review.md): run `scripts/deck-text.mjs` from this +skill's directory on `deck.pptx`, judge each flag, read the headline track, and for a +new deck or a substantial rewrite run one fresh-eye review by a separate agent. The SVG preview is a rendering aid, not a guarantee of PowerPoint fidelity or animation/media playback. Deliver the preview URL, the generated `deck.pptx` path and the editable project diff --git a/skill/references/review.md b/skill/references/review.md new file mode 100644 index 00000000..09b3fa71 --- /dev/null +++ b/skill/references/review.md @@ -0,0 +1,99 @@ +# Reviewing the words and the story + +Run this after the deck builds and the visual check passes, before delivering a new deck +or a substantial rewrite. For a small edit, run step 1 on the affected slides only. + +Detection is mechanical; judgement is yours. The model that wrote the deck cannot see its +own habits, so the checks list suspicions and a separate reviewer reads the deck cold. + +## 1. Extract the text and the flags + +From the slide project, after `npm run build`: + +```sh +node /scripts/deck-text.mjs deck.pptx --mode talk # or --mode document +node /scripts/deck-text.mjs deck.pptx --full # every text and note +``` + +The script prints the headline track, mode-specific checks and wording flags. It finds a +headline by the shape name `Headline`, so give each slide's headline `Text` the prop +`name="Headline"`. Without it the script falls back to a title placeholder or the largest +text and marks the guess with `?`. + +## 2. Judge every flag + +For each flag, decide "fix" or "keep (reason)". Keep a short ledger while doing so. + +- A flag inside a code sample, a quoted log or a product's official copy is kept. +- A flagged word that the project uses as a defined term is kept. +- Otherwise rewrite using [writing-ja.md](writing-ja.md) for Japanese wording, then rerun the + script to confirm the fix did not create a new flag. +- Do not rewrite in a new formula: replacing every 「〜ではなく」 with the same new pattern is + the same problem again. + +## 3. Read the headline track + +- **Talk**: read the headlines as the speaker's sequence of discoveries. Check that every + divider is answered by the next slide, nothing answers before it is asked, the mix of + forms is within reach of the reference ranges, and no pattern repeats three times in a + row ([story-talk.md](story-talk.md)). +- **Document**: read the headlines alone as one argument. Check that the answer comes first, + each headline is an assertion, peers share a form and nothing repeats + ([story-document.md](story-document.md)). + +Fix the storyline before polishing sentences; wording cannot rescue a missing step. + +## 4. Check facts across pages + +The mistakes readers and reviewers find first are not awkward words but pages that +contradict each other. The author misses them because each page was right when written. +List, in the ledger: + +- every quantity that appears on more than one page (counts of paths, layers, types; percentages; + dates), with each page's value. One quantity has one value, and a headline count matches + the items drawn on that slide; +- every mechanism explained on more than one page (where a check runs, what a marker is, + what a store holds), with each page's wording. They must describe the same thing; if the + design changed over time, say which version each page shows; +- every claim on the closing or summary slide, with the body page that supports it. A + summary that says "no manual steps" while body pages describe manual ones is a + contradiction, not a simplification; +- the central terms, with every variant used for each. + +Resolve each conflict from the source material. When the source does not settle it, weaken +the claim and ask the user; do not pick the version that reads better. + +## 5. Fresh-eye review + +Give the exported deck to a new agent that shares none of your context: do not mention how +the deck was made, the skill, the modes or what you were proud of. Replace `{FILE}`, `{N}` +and `{KIND}` (「登壇で話すための資料」 or 「読んで判断するための資料」) and pass this prompt: + +```text +あなたはプレゼン資料のレビュアーです。{KIND}({N} ページ)を読み、作った本人には見えない +破綻を指摘してください。どう作られたかは知らされません。 + +ファイル {FILE} を最後まで読み、全ページを順に確認してください。一部だけ読んで推測しない +こと。発表者ノートがあれば、話す内容として参照してかまいません。 + +1. 日本語の言い回し: 日本語話者が読んで引っかかる表現をすべて挙げる。不自然な語尾、口語と + 文語の混在、主語や述語が抜けた圧縮語、そろっていない箇条書きの語尾、長い連体修飾、 + 定義のない略語や造語、同じ概念の表記ゆれ、AI が書いたように感じる言い回し。 +2. 論理展開: 見出しだけを上から読み、話が飛ぶ・戻る・同じことを二度言う・前提なしに結論が + 出る箇所を挙げる。 +3. 破綻: 見出しの数と本文の数の食い違い、見出しと図の結論の食い違い、裏づけのない評価語 + (十分・問題ない・限定的)、既出ページの焼き直し、冒頭の予告と本編のずれ、同じ量が + ページごとに違う値、同じ仕組みの説明がページごとに違う箇所、まとめのページと本編の + 矛盾、数値の出所の欠落。 + +各指摘は「ページ番号・該当箇所の引用・何が問題か・言い換え案」で書く。最後に、直すべき順に +上位 5 件を挙げる。盛らずに書くこと。無傷の資料はないので、欠点を最低 3 つ挙げること。 +``` + +Tabulate the findings (page, finding verbatim, type, adopt / reject / hold, action or +reason). Decide each one; do not apply all of them. Show the user any finding that would +change meaning or structure and let them decide. Apply the adopted ones, rebuild, rerun +steps 1–4 and the visual check. + +One round is the default. Report the number of findings, adopted changes and remaining +concerns in three lines, and ask before running another round. diff --git a/skill/references/sources.md b/skill/references/sources.md new file mode 100644 index 00000000..115bbeaa --- /dev/null +++ b/skill/references/sources.md @@ -0,0 +1,40 @@ +# Sources and licences + +The storytelling and wording guidance in this skill adapts, translates and condenses rules +from the projects below. Each file was rewritten for office-kit; none is copied verbatim. +Measured figures (headline mix, seconds per slide) are the original authors' data. + +| Used in | Source | Licence | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| story-talk.md | [minorun365/minorun-marp-skill](https://github.com/minorun365/minorun-marp-skill) `skills/slide-story` (commit c6b20bb) | Apache-2.0 | +| story-talk.md, writing-ja.md | [minorun365/my-claude-code-settings](https://github.com/minorun365/my-claude-code-settings) `claude/skills/writing-guide` (commit 3bab8cd) | MIT, © 2026 Minoru Onda | +| story-document.md | [tyroneross/pyramid-principle](https://github.com/tyroneross/pyramid-principle) `pyramid-presentation`, `pyramid-principle-core` (commit d1343aa) | Apache-2.0 | +| story-document.md, writing-ja.md, review.md, deck-text.mjs | [carnot-tech/consulting-pptx-skill](https://github.com/carnot-tech/consulting-pptx-skill) `references/slide-rules.md`, `ai-smell-lexicon.md`, `content-review-prompt.md` (commit f50edac) | MIT, © 2026 Carnot AI Inc. | +| story-document.md, writing-ja.md, review.md | [coji/natural-japanese](https://github.com/coji/natural-japanese) `writing-constitution.md`, `forbidden-patterns.md`, `translationese.md`, `doctypes/slide.md` (commit 9a78a42) | MIT, © 2026 coji | + +Changes made for this skill: translated between English and Japanese, merged overlapping +rules, removed rules specific to Marp, HTML output, book publishing and the original +authors' brand design, split the rules into talk and document modes, and reimplemented +the text checks in JavaScript against `@office-kit/pptx`. + +The MIT-licensed sources are provided under this notice: + +> Permission is hereby granted, free of charge, to any person obtaining a copy of this +> software and associated documentation files (the "Software"), to deal in the Software +> without restriction, including without limitation the rights to use, copy, modify, merge, +> publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +> to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or +> substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +> INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +> PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +> FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +> OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +> DEALINGS IN THE SOFTWARE. + +The Apache-2.0 sources are used under the +[Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0); the changes are +listed above. diff --git a/skill/references/story-document.md b/skill/references/story-document.md new file mode 100644 index 00000000..bdfeea76 --- /dev/null +++ b/skill/references/story-document.md @@ -0,0 +1,145 @@ +# Document mode: decks people read or decide from + +A document deck is read without its author, or used in a meeting to reach a decision: +reports, proposals, decision memos, board packs, handouts. The reader wants the answer +first and must be able to follow the argument from the headlines alone. + +The structure rules come from Barbara Minto's pyramid principle as operationalised by +tyroneross/pyramid-principle; the headline, bullet and table conventions come from +carnot-tech/consulting-pptx-skill (rules distilled from real consulting review +comments); message-line and density rules come from coji/natural-japanese. See +[sources](sources.md). When the deck is Japanese, also apply [writing-ja.md](writing-ja.md). + +## Before outlining + +1. Establish who reads it, in which meeting, and what they must understand or decide. If + that is unknown, ask. If you cannot ask, state the reader you assumed when you deliver. + When even the purpose is unclear, offer a one-page summary of the issues first: every + extra slide adds exposure to errors and filler. +2. State the governing thought in one sentence: the decision, recommendation or finding. + If you cannot, the material is insufficient; ask for it rather than writing around it. +3. Collect the facts the argument needs. Use only what the user supplied or you verified. + A missing number or name is asked for, or shown as a visible `[要確認: …]` + placeholder. Never invent a figure, customer, quote or source. +4. Write the storyline: one line per slide, in order. Lines include premises, facts and + questions as well as answers; read in sequence they form one speech. Agree it with the + user before building a deck of more than about ten slides. +5. Derive the slide count from the storyline. Each slide makes one claim, so count the + claims each group of support needs (a mechanism a reader would copy is usually one + claim), then add the opening and close. Never pad to a target count or drop a + supported point to meet one. + +## Order + +- Give the governing thought before the evidence. Add only the minimum situation and + complication the reader needs to understand it (SCQA), then the answer. +- An executive summary, when used, maps one row to each body slide or chapter, in the + same order: one main sentence plus 2–3 details, each from a different angle. +- Each group of peer slides answers one parent question and plays one role (reasons with + reasons, steps with steps). Choose one order per group: deductive, chronological, + structural or by importance. +- Give important groups depth and light ones a light touch. Do not make every slide the + same density or force every list to three items; one point is fine when one suffices. +- End the body with the governing thought at the confidence the evidence supports, and + the decision or next action you need from the reader. Not "thank you", not a + restatement. Appendix slides (company profile, hiring, detailed tables) may follow it. +- When facts from before and after a change (a migration, a redesign) sit in one story, + say on the page which point in time it describes. +- Readers navigate by page: show page numbers, and let the executive summary rows point + to page ranges (「→ p.15–24」). A source line at the bottom of the slide that uses a + number or cites another product is part of the slide, not clutter. + +## Headlines + +Every slide headline is an assertion the reader can agree or disagree with. + +- Test 1: can the reader respond "I disagree"? If the only response is "I see what this + section is about", it is a label. Rewrite it. +- Test 2: hide the body. Does the headline still carry the slide's point? If not, the body + was carrying the argument. +- Exceptions: cover, section entrances and company introductions use plain labels + (「会社概要」). A company introduction written as a claim becomes self-praise. +- A chart slide states what the chart means (the "so what"), not what it plots. +- Use specific nouns, real names and numbers, and verbs that say what happened + (伸びた/止めた/上回った), not 「がある」「を示す」「を提供する」. +- Do not claim more than the evidence supports, especially about the reader's own + organisation or your own. Narrow the claim to what the body proves. +- Put a count in a headline only when the number is the point (「価格は 8 倍開く」). Element + counts such as 「3 つの理由」「5 段階」 are not claims; state the content instead. When a + number does appear, it must match the body. +- No topic label before a colon (「現状と課題:〜」). The topic tag lives in a small + kicker; the headline starts with the subject. +- A headline stands on its own: no 「この/その/ここまで」 pointing at another slide. +- Peer slides in one group share a grammatical form, even when the group is long; this + overrides the "no mould three times in a row" rule in writing-ja.md for headlines. + Across groups, forms naturally vary with role (facts, questions, premises); a whole + deck of 「〜は、…する」 is a template being copied. +- If no assertion can be written, the slide is either not ready (form the claim first) + or not needed (merge or cut it). A slide that only says "now the financials" is a + transition that escaped into its own slide. + +For Japanese headlines: plain form, never ending in です/ます (the same holds for the +cover note, footnotes and appendix text you write); two rendered lines at +most (about 40 full-width characters per line at title size, product names in Latin +letters count about half); when it wraps, put an explicit line break at a meaning +boundary and never leave 1–3 characters on the last line; a normal sentence with subject +and object, never compressed into a slogan to fit one line. Do not colour parts of a +headline. + +## Turning a talk deck into a document + +Keep the facts, numbers and history; rebuild the order and wording answer-first. + +- Drop agenda, divider and "what comes next" slides; the summary rows and page numbers do + their job. +- Move what only the speaker notes say into the body when the reader needs it, including + limitations the speaker had saved for Q&A: a reader cannot ask. +- Put the speaker and company introductions in an appendix unless the reader needs them + to trust the argument; keep official copy verbatim. +- Recheck numbers the notes flag as "verify before the talk"; a published document keeps + them longer. + +## Body text + +- One slide, one message. Bullets list only what is truly parallel; cause and sequence + are written as a sentence. +- Within one level, bullets end the same way (all noun endings or all verb endings), never + です/ます. Different levels may use different endings. +- Keep subject and verb: 「ユーザーが手動で作成する」, not 「手動作成」; 「AI が不備を検出し、 + 担当者が確認する」, not 「精度向上」. Do not create passive sentences with abstract nouns as + subjects. +- Each bullet adds a new angle (cost, time, people, decision, means, volume). Delete any + bullet whose removal loses no information, and anything the reader already knows. +- One concept, one term, throughout the deck. Expand an abbreviation at first use. Put a + metaphorical term in 「」 and define it once. Do not bring internal jargon into an + external deck. +- Say from whose viewpoint relative words hold (社内/外部/先方/現場). +- Number or label items only when a later slide refers back to that number. +- Use parentheses sparingly; do not attach a one-line caption that says what the figure + already shows. +- For automated behaviour, name the human decision next to it: 「AI が改善案を出し、採用は + 担当者が決める」. + +## Tables + +- Keep only columns that serve the headline's claim; a column invites the comparison the + reader will make. +- Rows are members of the set the row-axis defines. Do not slip a conclusion or a + different category in as a row. +- Column headers are concrete nouns that name what the cells hold, not 「事実」「持っているもの」 + and not conclusions. +- Keep each column at one grain and one viewpoint; write 「—」 for non-applicable cells. +- In a table that shows changes, list only what changed. + +## Final check for document decks + +1. Read the headlines alone, top to bottom (`deck-text.mjs --mode document`). They must form + one argument with the answer first, no jumps, repeats or conclusions without premises. +2. Every non-exempt headline passes both assertion tests. +3. Numbers and listed items in headlines match their bodies one to one, and numbers, mechanisms and terms agree across + pages ([review.md](review.md) step 4). The closing summary claims nothing the body + contradicts. +4. Evaluative words (限定的, 十分, 問題ない) are backed on the same slide. +5. Later slides add new points instead of restating earlier ones. +6. The last slide asks for a decision or action. +7. A fresh-eye review has been run ([review.md](review.md)). diff --git a/skill/references/story-talk.md b/skill/references/story-talk.md new file mode 100644 index 00000000..cdd38f2e --- /dev/null +++ b/skill/references/story-talk.md @@ -0,0 +1,206 @@ +# Talk mode: slides a speaker presents + +A talk deck is a picture story the speaker talks over. The audience sees each slide for +seconds and must always know where in the story they are. It is not a document: what +the speaker says goes in the notes, and the slide carries only what must be seen. +Coverage, caveats and footnotes that a reader would need do not belong on it. + +Most rules below come from minorun365's slide-story and writing-guide skills, which +were distilled from years of the author's own conference decks. See +[sources](sources.md). When the deck is Japanese, also apply [writing-ja.md](writing-ja.md); +where the two differ, this file wins for headlines. + +## Before outlining + +1. Confirm the official title, abstract and time slot from the event page. Everything the + abstract promises must appear in the talk; check this at the outline and at delivery. +2. Ask the speaker what the audience should take home, who the audience is, and where the + talk should surprise them. Collect the speaker's real material first: incidents, + numbers, screenshots, logs, past decks on the same topic. A structure cannot generate + the weight of a real story; never invent anecdotes, quotes or figures to fill a slot. +3. If the speaker has a past deck on the same subject, build from it: keep content, order + and phrasing, and change only stale facts, minimal bridges and slide-level cuts. + Present candidate cuts to the speaker as blocks of neighbouring slides, not as a list. + When the user asks for the deck to be restructured, keep only its facts, numbers and + history (the speaker notes are often the richest source) and rebuild order and wording. +4. Agree the picture-story outline (one line per slide) with the speaker before styling. + +Do not pour content into a template's shape. Three "icon + bold heading + one line" cards +repeated on every slide looks finished and says nothing. Do not import document devices +(cards, comparison grids, hub diagrams) that put four thin blocks on one slide. + +## Shape of the story + +**Open from where the audience is**, not from a definition: voice their problem +(「最近 MCP ってよく聞きますよね!…分かりづらくないですか?」), a provocative quiz, a news hook, +or the speaker's own recent experience. After the self-introduction, one or two questions +to the audience work as the entrance to the body. + +**Do not give the answer first.** Revealing information one step at a time keeps the +cognitive load low and the talk interesting. + +- No agenda or "today I will talk about three things" slide, no upfront conclusion, no + overview map at the start. +- When a slide asks a question, answer it on the following slides. Do not ask and answer + on the same slide. +- Before adding a slide, check it is not a summary of what comes later. +- The talk still has one governing argument; keep it in the outline and the notes. The + slides show one discovery at a time. If you want to write 「〜の 2 つです」 or + 「あとで証明します」 on a slide, it has become a declaration. + +**Section dividers voice the audience's next question** instead of naming a chapter: +「え、じゃあ AI エージェントって何なの…?」「よし、書けた!…どこにデプロイする?」. + +- The slide right after a divider starts answering it, and the answer is complete before + the next divider. Delete dividers nothing answers. +- Only the question is on the divider: no number, sub-line or chapter label. At a large + size it may wrap into two lines at a meaning boundary. +- Place one every 3–6 body slides; never more than two in a row. An opening question to + the audience counts as a body slide. +- Never ask the same question twice to keep that rhythm. When one question genuinely + covers a long stretch, a longer gap is better than a repeated divider. +- When the story spans a change (a migration, a redesign), say on each affected slide + whether it shows before or after the change. +- With three or more chapters, show where the audience is with a small, quiet step bar + at the top of the body slides (not on the divider and not as an extra slide). It + shows position, never answers. + +**Introduce a concept in three steps**: what it is (one-line definition, etymology if it +helps), what it makes better (before/after), then a rough one-liner. Explain abstract → +concrete → analogy. An analogy explains; it never decorates. After an analogy, derive the +next slide from it instead of switching to another framing. + +**Body flow** that works for most technical talks: a concrete example first ("this is what +you can build") → the limit of the current way → the mechanism (one diagram) → how to do +it → where people get stuck → what solves it → the next step. Keep each claim next to its +example; do not put another topic between them. + +**Staging**: use at least two or three of these per deck, from the speaker's real material. +A deck with none reads as a model student's. + +- A diagram built up over 3–5 slides instead of shown complete +- A false ending and reversal +- A short dialogue that turns a protocol or negotiation into speech bubbles +- Anticipating a misconception (「もしかして、これを思い浮かべていませんか…?」) +- Objection and rebuttal in quick succession +- At least one honest limitation slide; never sell only +- The speaker's own name for an experience + +**Close with an action**, not a summary slide: a call to action for the audience, then the +promotion or book cover if any. Do not put a "thank you" slide before a promotion slide. +If the material has no call to action, derive it from what the speaker said the audience +should take home, and ask the speaker to confirm the wording. + +## One slide + +| Item | Rule | +| -------- | -------------------------------------------------------------------------------------------- | +| Amount | One headline and 3–4 lines of body. Split beyond that. | +| Headline | Tells what the slide is about the moment it appears. One line; rephrase instead of wrapping. | +| Body | 3–4 items, one level, 1–2 sentences each. | +| Emphasis | At most one per slide; across the deck, about one per 2–3 body slides. | +| Figure | One per slide, large. A figure-only slide may reuse the previous headline. | +| Table | 2–3 simple columns. | + +Official copy the speaker must use verbatim (company or product introductions) is exempt +from the amount rule; keep it on as few slides as the copy allows. + +When a figure is the star, text is the headline plus at most one intro line; say the +conclusion aloud. Do not add small text for what the screen already shows: captions +under logos or screenshots, grey afterthought lines, coloured closing lines, notes under +tables. The test: "Can the audience see it on screen already, or would it change their +judgement today?" A bottom band that restates the slide in other words is the same +mistake. One exception: if the deck will be published, a claim about someone else's +product may carry a single small source line, because the slide will be read without the +speaker. + +## Headlines: mix the forms, then count + +All-polite or all-plain headlines both look machine-made. Decide the form per slide from +its role before writing it; do not write neutral text and convert the ending afterwards. + +| Role of the slide | Form | Example | +| ---------------------------------------- | ------------------------------------ | ------------------------------------- | +| A figure, code or screenshot is the star | Short noun label (3–6 chars is fine) | こうなりがち/アーキテクチャ例 | +| A claim or finding | Plain-form statement | 令和の AI エージェントは 3 行で書ける | +| Surprise, good news, a turn | Exclamation | デフォルトでストリーミング対応! | +| The audience's inner voice | Spoken monologue | よし、エージェント書けた! | +| A rebuttal, promotion or close | Polite form | …人生そんなに簡単じゃないんです | +| A bridge to the next slide | Cut off mid-thought | 技術的な下地は整ったが… | +| Introducing a term | Descriptive phrase + 「term」 | AI に記憶をもたせる「メモリー」 | +| Divider question | 「〜の?」 | 何を用意すればいいの? | + +Two kinds of endings sound wrong in Japanese: + +- A polite-form sentence with only its ending swapped to plain form (割に合いません → + 割に合わない). Rebuild it as a noun ending, 「〜しよう」 or 「!」 instead. +- Contracted colloquial endings (〜てる, 〜ばいい, 〜んです) used to sound friendly. + Use 「OK」, 「〜の?」 or 「〜しよう」. + +Uniformity is invisible slide by slide and obvious in a list. After drafting, run +`deck-text.mjs --mode talk` ([review.md](review.md)) and compare with hand-made decks by +the same author (two decks of 57–58 slides; the percentages count every slide, +dividers included): + +| Metric | Hand-made | Machine-uniform | +| --------------------- | --------- | -------------------------- | +| Headlines with 「!」 | 7–20% | 1% | +| Headlines with 「?」 | 10–15% | 7% | +| Polite-form headlines | 10–12% | 1% | +| Shortest headline | 3–6 chars | every headline 15–24 chars | + +These are one speaker's numbers: treat a large gap as a signal to reread, not a quota. +Many question dividers push 「?」 above the range; that is fine when the questions differ +in shape. +Also look for the same pattern three slides in a row: comma-pause two-part headlines +(「覚える言葉は、この 3 つだけ」), headlines that start with the same connective +(しかも/実は/ちなみに), and colon headlines (「品質:」「量:」). + +Slides are not README headings. On a slide, 「簡単」「〜するだけ!」 are legitimate: the +headline's job is to carry the audience to the next point, not to label a section. + +## Quotes and speech bubbles + +- Quote only from real logs. A plausible sentence presented as "what I actually typed" is + fabrication; without a real source, rewrite the slide so it no longer claims one. +- Keep real quotes verbatim; do not polish them for looks. +- Do not put a clever aphorism in a coloured quote box. Boxes hold concrete steps, numbers, + examples or real prompts. +- A speech bubble must not restate the slide's conclusion. It carries a concrete honest + aside that the bullets do not. + +## Notes and timing + +Put the spoken explanation, sources and caveats in the slide notes (`Slide notes`), in +the speaker's voice. The speaker, not the slide, says the details. + +Estimate the finish time when the outline settles, whenever the slide count moves by 5+, +and before delivery. Count body slides separately from dividers and the cover (about 8 +seconds each). + +| Setting | Seconds per body slide | +| ----------------------------------- | ---------------------- | +| 5-minute lightning talk | ~30 | +| ~15-minute slot, screenshot-heavy | ~29 | +| Developer conference, ~40 minutes | ~37 | +| Exhibition or non-engineer audience | ~46 | + +The unit times exclude demos and Q&A: add those as fixed blocks (+0.5 min per live demo +for switching). If you do not know whether Q&A is inside the slot, ask; if you cannot, +plan the talk for the slot minus 5 minutes and say so. Aim to finish about 5 minutes +early in a venue. Rather than cutting early, mark up to two checkpoints: +"if we pass this divider after N minutes, skip that slide later". Never skip a divider. +Replace these unit times with the speaker's own once measured. + +## Final check for talk decks + +1. No slide exceeds one headline plus 3–4 body lines. +2. No headline is a bare topic label unless a figure is the star. +3. No slide has two figures or two emphasised spots. +4. Every divider starts being answered on the next slide and is fully answered before the + next divider; no question is asked twice. +5. No agenda, upfront summary or overview-as-answer slipped in. +6. The headline mix and pattern runs look like a person's, not a template's. +7. The abstract's promises all appear, the title's promise is paid off by the end, and the + time estimate fits the slot. +8. Numbers, mechanisms and terms agree across pages ([review.md](review.md) step 4). diff --git a/skill/references/tsx.md b/skill/references/tsx.md index be65d2f1..bf8cef96 100644 --- a/skill/references/tsx.md +++ b/skill/references/tsx.md @@ -57,7 +57,7 @@ export default ( ['Ben', 'Ship annual billing', 'Dec 1'], ]} columnWidths={[2.2, 7, 2.3]} - cellStyle={{ format: { size: 16, color: '#15171C' } }} + cellStyle={{ fill: '#FFFFFF', format: { size: 16, color: '#15171C' } }} headerStyle={{ fill: '#15171C', format: { color: '#FFFFFF', bold: true } }} stripeFill="#F3F4F7" /> @@ -69,6 +69,18 @@ export default ( Use functions, fragments, arrays, conditions and `.map()` to compose content. Bounds (`x`, `y`, `width`, `height`) are required for new visual objects. +Details that trip up type checking and layout: + +- `anchor` takes `'top' | 'center' | 'bottom'`. +- Colors are the core `Color` type (`#RRGGBB` or a theme token); type your own helper + props with `Color` from `@office-kit/pptx`, not `string`. +- The initialized project enables `exactOptionalPropertyTypes`; declare an optional helper + prop as `name?: T | undefined` when callers may pass `undefined`. +- A newline in `Text` children starts a new paragraph. Use it to break a long headline at + a meaning boundary instead of letting the renderer wrap it anywhere. +- `stripeFill` colors every second body row only. Set `cellStyle.fill` as well, or the + other rows keep the table style's default banding. + | Element | Inputs | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Presentation` | Optional `source` bytes, new-deck `size`, `theme`, `mode`. | @@ -243,7 +255,9 @@ export default ( ``` Each function returns a `Slide`. Use `theme.ts` for shared palette/typography -values; import only what the slide needs. Use descriptive filenames that stay -stable when slides move. Patch text, props or the relevant data item for local -changes; change a shared value only when all its consumers should change. +values; import only what the slide needs. For page numbers, keep the slide order in one +array in `deck.tsx` and pass the position: `{slides.map((S, i) => )}`. +Use descriptive filenames that stay stable when slides move. Patch text, props or the +relevant data item for local changes; change a shared value only when all its +consumers should change. Existing inline slides and data-driven compositions remain valid. diff --git a/skill/references/writing-ja.md b/skill/references/writing-ja.md new file mode 100644 index 00000000..5315046a --- /dev/null +++ b/skill/references/writing-ja.md @@ -0,0 +1,112 @@ +# 日本語スライドの文言 + +日本語のスライドを書くとき、アウトライン案や見出し案をユーザーに見せる段階から適用する。 +AI が書いた日本語が読みにくいのは、禁止語よりも「均一さ」「決め台詞」「中身のない語」 +「英語の直訳」から来ることが多い。語を差し替えるだけでは直らないので、理由を理解して使う。 + +構成と見出しの型はモード別のファイル([登壇](story-talk.md)/[読ませる資料](story-document.md)) +が決める。この文書と食い違う場合、見出しの文体はモード別ファイルが優先する。 + +出典は coji/natural-japanese(文体憲法・禁止語・翻訳調)、minorun365 の writing-guide +(再発パターン)、carnot-tech/consulting-pptx-skill(AI 臭ワード集・文章規約)。詳細は +[sources](sources.md)。 + +## 1. 書く前の原則 + +1. **前置きを書かない。** 「本日は〜についてご紹介します」「近年〜が注目されています」は + 何も運ばない。いきなり中身に入る。 +2. **固有名詞と数字で接地する。** 「一部の顧客」でなく「A 社と B 社」、「大幅に」でなく + 「約 60%」。固有名詞・数字・実例を抜いても意味が変わらない文は一般論のまま。 + 素材が無ければ書き手が埋めず、ユーザーに聞くか `[要確認: …]` を見える形で残す。 +3. **用語は、働きを説明してから名前を渡す。** 略語は初出で正式名を併記する。 + 「その語を IT 以外の同僚が会話で使うか」で注釈の要否を判断する。 +4. **行為者を主語にする。** 「データが示している」「文化が醸成される」のような無生物主語や、 + 抽象名詞を主語にした受け身は、誰が何をするかの文に開く。 +5. **強調は 1 スライド 1 か所。** 太字・色文字を散らすと、どこも強調に見えなくなる。 +6. **濃淡をつける。** 重要なスライドは厚く、軽いものは軽く。すべての箇条書きを 3 点に + そろえない。1 点で足りれば 1 点。 +7. **同じ鋳型を 3 回続けない。** 見出しの文型、箇条書きの書き出し、スライドの構成 + (「一般には→自社では→だから」)が 3 回続いたら、少なくとも 1 つは入口を変える。 + ただし読ませる資料で、同じ問いに答える一群のスライドの見出しは文型をそろえる + ([story-document.md](story-document.md) が優先)。 +8. **「〜ではなく〜」は、聞き手が本当に誤解していそうなときだけ使う。** 後半だけを見せて + 意味が通るなら対比は要らない。デッキ全体で数回までにとどめる。 +9. **確信度は語尾でぼかさず、明示する。** 「〜と思われます」をまぶさない。推定は「推定」、 + 未確認は `[要確認]` と書く。事実には出典、意見には話者を添える。 +10. **主張の広さを根拠に合わせる。** 「必ず」「どれでも」のような全称は、例外に当たった + 読み手の信頼を失う。 + +## 2. 避ける表現 + +`deck-text.mjs` がこれらを機械的に拾う。検出は疑いの提示なので、1 件ずつ文脈で判断する +([review.md](review.md))。そのプロジェクトで正式に採用している用語は対象外。 + +| 型 | 例 | 直し方 | +| -------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| 「効く」の全用法 | 効く/効かない/効いてくる/効かせる | 主体と結果を書く(「事前集計が使えない」「RLS がもう一度適用される」) | +| ダッシュ | —、──、―(目次の行頭記号も含む) | 読点、括弧、文を分ける。目次は記号なしか「・」 | +| 締めの決め文・回収文 | 「今日の話は、これに尽きます」「これが◯◯の形です」「〜のは◯◯です」 | 削除。直前の文で言い終えている | +| こなれた比喩動詞 | 届かない/拾ってくれる/地続き/一撃で/仕込む/握る/差し込む | 平明な動詞(気づける・設定する・追加する) | +| 行頭の「ラベル:説明」 | 「品質:〜」「履歴:〜」を並べる | 見出しと本文に分ける、表にする、1 文に戻す | +| 主張のあとの否定の但し書き | 「〜するだけです。外部には何も送りません」 | 主文で終える。必要なら両側を 1 文に(「A は止め、B は通す」) | +| 空虚な強調 | 非常に/極めて/画期的/革新的/圧倒的/〜と言っても過言ではない/〜に他ならない | 削る。強調したいなら数字か事実で | +| カタカナ盛り | ソリューション/シナジー/シームレス/エンドツーエンド/付加価値 | 具体的に何をするかを書く | +| 名詞化したビジネス語 | 活用/推進/実現/創出/担保/強化/最適化/効率化 | 素の動詞に戻す(活用する→使う、実現する→できる) | +| 圧縮語 | 手動作成/精度向上/連携強化 | 主語と述語のある形に開く | +| 定型の枕・予告 | 昨今/近年〜/結論から言うと/以下の通り/理由は 3 つあります/見ていきましょう/深掘り/正面から扱う | 削る。数を先に宣言せず中身から入る | +| 結論の押し付け・ぼかし | 〜と言えるでしょう/〜のではないでしょうか/〜と考えられます(の連発) | 言い切るか、条件を具体的に書く | +| 空虚な形容 | 不可欠/核心的/鍵となる/肝となる/多角的/包括的 | 何がどう必要かを書く | +| 翻訳調 | することができる/することが可能/という観点から/にとって重要/意味を持つ/することによって | できる/〜で見ると/〜には〜が要る/意味がある/〜すると | +| 過大な効能 | これだけで/一気に/劇的に/驚くほど | 数字にするか、何が減る・何ができるかを書く | +| 造語・命名の儀式 | 「◯◯らしさ」「これを本資料では◯◯と呼ぶ」「◯◯×△△」 | ラベルを作らず普通に説明する | +| 動詞 3 点セットのラベル | 「考える、動く、振り返る」 | 文で説明する | +| 対句・標語 | 「使いながら守り、守りながら使う」「A できないものを A したように見せない」 | 本当に何度も参照する標語だけ 1 つ残す | +| 強調の複合動詞 | 守り切る/止め切る/閉じ切る/言い切る | 「必ず拒否する」など、何をどこまでするかを書く | +| 向きの比喩 | 安全側に倒す/閉じる側に倒れる/null に倒す | 何が起きるかを書く(「見えなくなるが漏れない」「値を破棄する」) | +| 決め台詞語彙の使い回し | 〜の正体/本丸/生命線/王道/種明かし/〜こそ | デッキ全体で 2 回まで | +| 許可型 | 〜で構いません | 「〜でも OK」「〜で十分」か言い切り | +| スライドを「枚」と呼ぶ | 次の枚/3 枚目 | 「次のスライド」「3 ページ目」。画像やカードの個数は「枚」でよい | +| 誰から見てか不明な相対語 | 外部/社内/現場/先方 | 主体を名指しする | +| 絵文字の装飾 | 見出しや箇条書きの頭の絵文字 | 付けない | + +長さそのものは問題ではない。「短く書け」ではなく、決め台詞・効能の請け合い・まとめの +押し付け・中身の空疎さを消す。言葉を足して正確になるなら足す。迷ったら声に出して読み、 +自然なほうを選ぶ。 + +## 3. デッキ全体で出る癖 + +1 枚ずつ読むと自然でも、並べると機械的に見えるもの。最後に全体を並べて確かめる。 + +- 全スライドの下端に、本文を言い換えただけの「まとめ帯」がある +- すべての箇条書きがちょうど 3 点、すべてのカードが同じ行数 +- 見出しがすべて同じ文型(全部体言止め、全部「〜は、…する」、全部コロン付き) +- 同じ接続詞(しかも・実は・ただし)や「〜したいのに、〜」の型で始まるスライドが続く +- 同じ主張を別の言い方で何度も言う(課題の説明が 4 回出てくる、など) +- 箇条書きや体言止めの列挙を「。」で締める + +## 4. 表記 + +- 1 つの資料で 1 つの用語。ユーザー/利用者、紐づけ/紐付け、メモリ/メモリー を混ぜない。 + 書き始める前に、中心になる概念(登場人物、データの置き場所、仕組みの部品)の呼び名を + 決めて並べておく。人の呼び方は特に揺れやすいので、B2B の資料では契約する側(顧客)と + 実際に使う人(利用者)を最初に決める。同じものを「分析ストア」「複製先」「OLAP」と呼び分けると、読み手は + 別のものだと受け取る。逆に 1 つの語を 2 つの意味で使わない(「条件」が権限の条件式と + 前提条件の両方を指す、など)。 +- 助詞や表記の揺れ(つき/付き、無い/ない、・/・)もそろえる。 +- 製品名はアルファベットのまま書く(ClickHouse をカタカナにしない)。 +- スライドの箇条書きとラベルには句点を付けない。文として書く本文はそろえる。 +- 丸括弧を多用しない。 +- 日本語の長文を書いた直後は、ハングル・キリル文字の混入を確認する(目視では気づけない)。 + +## 5. 既存の資料を直すとき + +- 既定は「残す」。読み手の得が具体的に言える箇所だけ直す。元の話し手の言い回しや実体験は + 資産であり、均して消さない。 +- 同じ種類の修正(見出しの主張化、箇条書きの文章化など)を全スライドに一律に当てない。 + 個々の変換が正しくても、全体としては元より機械的に見える。見出しを主張化するのは、 + 中身を探す手間が実際に大きい数か所にとどめる。 +- 元の言い回しを残すのは、同じ資料の事実と食い違わない場合に限る。「手作業は一切ない」 + のような決め文句が本文の手順と矛盾するなら、事実に合わせて直す。 +- 会社紹介など正式な定型文言は、正本の側で直すものとして残す。 +- 構成(順序・枚数)は、依頼されたときだけ変える。変えたほうがよい点は提案として伝える。 +- 見出しを変えたら、発表者ノートの同じ語句も合わせて直す。 diff --git a/skill/scripts/deck-text.mjs b/skill/scripts/deck-text.mjs new file mode 100644 index 00000000..58de4373 --- /dev/null +++ b/skill/scripts/deck-text.mjs @@ -0,0 +1,295 @@ +#!/usr/bin/env node +// Prints a deck's headline track and flags wording for review. +// Usage (from the slide project): node /scripts/deck-text.mjs deck.pptx [--mode talk|document] [--full] +// +// Flags are suspicions, not verdicts: references/review.md explains how to judge each one. +import { readFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { parseArgs } from 'node:util'; + +const USAGE = 'Usage: deck-text.mjs [--mode talk|document] [--full]'; +const MODES = ['talk', 'document']; +const HEADLINE_NAME = 'Headline'; +// Longer text is body copy even when it is set large. +const GUESS_MAX_LENGTH = 80; +const MONOSPACE = /mono|courier|consolas|menlo|code|等幅/i; + +const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { mode: { type: 'string' }, full: { type: 'boolean', default: false } }, +}); +const [file] = positionals; +const mode = values.mode ?? null; +if (positionals.length !== 1 || (mode !== null && !MODES.includes(mode))) { + console.error(USAGE); + process.exit(2); +} +const full = values.full; + +const pptx = await import(await resolveCore(process.cwd())); +const presentation = await pptx.loadPresentation(await readFile(file)); + +// The script ships with the skill, outside the slide project, so a bare import +// would resolve against the skill directory. Resolve the project's installed copy. +async function resolveCore(start) { + for (let directory = start; ; directory = dirname(directory)) { + const own = join(directory, 'package.json'); + if (existsSync(own) && JSON.parse(await readFile(own, 'utf8')).name === '@office-kit/pptx') + return entryOf(directory); + const installed = join(directory, 'node_modules', '@office-kit', 'pptx'); + if (existsSync(installed)) return entryOf(installed); + if (dirname(directory) === directory) + throw new Error('@office-kit/pptx is not installed; run this from the slide project.'); + } +} + +async function entryOf(packageDirectory) { + const manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8')); + return pathToFileURL(join(packageDirectory, manifest.exports['.'].import)).href; +} + +function readSlide(slide, index) { + const shapes = []; + for (const shape of pptx.getSlideShapes(slide)) { + if (pptx.isTableShape(shape)) { + for (const row of pptx.getTableCells(shape)) + shapes.push({ text: row.map((cell) => pptx.getTableCellText(cell)).join(' | ') }); + continue; + } + const text = pptx.getShapeText(shape).trim(); + if (text) shapes.push({ shape, text, code: isCode(shape) }); + } + const headline = + shapes.find(({ shape }) => shape && pptx.getShapeName(shape) === HEADLINE_NAME) ?? + placeholderTitle(slide, shapes) ?? + largestText(shapes); + return { + number: index + 1, + headline: headline ? oneLine(headline.text) : null, + guessed: headline?.guessed ?? false, + headlineRaw: headline?.text ?? null, + texts: shapes.filter((entry) => entry !== headline && !entry.code).map(({ text }) => text), + code: shapes.filter((entry) => entry.code).map(({ text }) => text), + notes: pptx.getSlideNotes(slide) ?? '', + }; +} + +// Code samples and prompt examples are quoted verbatim; wording flags there are noise. +function isCode(shape) { + const format = pptx.getShapeRunFormatEffective(presentation, shape, 0, 0); + return MONOSPACE.test(`${format?.font ?? ''} ${format?.fontEastAsian ?? ''}`); +} + +function placeholderTitle(slide, shapes) { + const title = pptx.getSlideTitle(slide)?.trim(); + return title ? shapes.find((entry) => entry.text === title) : undefined; +} + +// Decks made elsewhere rarely name their headline; the biggest short text is the best guess. +function largestText(shapes) { + let best; + let bestSize = 0; + for (const entry of shapes) { + if (!entry.shape || entry.text.length > GUESS_MAX_LENGTH) continue; + const size = pptx.getShapeRunFormatEffective(presentation, entry.shape, 0, 0)?.size ?? 0; + if (size > bestSize) [best, bestSize] = [entry, size]; + } + if (best) best.guessed = true; + return best; +} + +function oneLine(text) { + return text.replace(/\s*[\n\v]\s*/g, ' '); +} + +function label(slide) { + return `p${String(slide.number).padStart(2, '0')}`; +} + +function printHeadlines(all) { + console.log('# Headline track'); + const missing = all.filter((slide) => slide.headline === null).length; + for (const slide of all) + console.log(`${label(slide)}${slide.guessed ? '?' : ' '} ${slide.headline ?? '(no headline)'}`); + if (all.some((slide) => slide.guessed)) + console.log( + `\n"?" marks a guessed headline (largest text). Name the headline Text "${HEADLINE_NAME}".`, + ); + if (missing > 0) + console.log( + `\n${missing} slide(s) have no headline. Name the headline Text "${HEADLINE_NAME}" (or use a title placeholder).`, + ); +} + +function printFullText(all) { + console.log('\n# Slide text'); + for (const slide of all) { + console.log(`\n## ${label(slide)} ${slide.headline ?? ''}`); + for (const text of slide.texts) console.log(`- ${oneLine(text)}`); + for (const text of slide.code) console.log(`- [code] ${oneLine(text)}`); + if (slide.notes) console.log(` notes: ${oneLine(slide.notes)}`); + } +} + +// Measured on hand-made Japanese conference decks (minorun365/minorun-marp-skill, +// slide-story). A deck far below these ranges reads as machine-uniform. +const TALK_RANGES = { exclamation: [7, 20], question: [10, 15], polite: [10, 12] }; +const POLITE_END = /(です|ます|ました|ません|でした|でしょう|ください)か?[。!?!?…]*$/; +const COLLOQUIAL_END = /(てる|ばいい|んです)[。!?!?]*$/; +const LEADING_CONNECTIVE = /^(しかも|実は|ちなみに|ただし|例えば|まずは|最後に|そして|さらに)/; +const COLON_HEADLINE = /[::]/; + +function printTalkMetrics(all) { + const headlines = all.filter((slide) => slide.headline !== null); + if (headlines.length === 0) return; + const share = (test) => + Math.round((headlines.filter((slide) => test(slide.headline)).length / headlines.length) * 100); + const shortest = headlines.reduce((a, b) => (a.headline.length <= b.headline.length ? a : b)); + console.log('\n# Talk headline mix (hand-made reference range in brackets)'); + console.log(`! ${share((h) => /[!!]/.test(h))}% [${TALK_RANGES.exclamation.join('-')}%]`); + console.log(`? ${share((h) => /[??]/.test(h))}% [${TALK_RANGES.question.join('-')}%]`); + console.log( + `polite form ${share((h) => POLITE_END.test(h))}% [${TALK_RANGES.polite.join('-')}%]`, + ); + console.log( + `shortest ${shortest.headline.length} chars (${label(shortest)}) [3-6]; median ${median(headlines.map((s) => s.headline.length))}`, + ); + for (const slide of headlines) + if (COLLOQUIAL_END.test(slide.headline)) + console.log(`${label(slide)} colloquial ending: ${slide.headline}`); + reportRuns(headlines, 'colon headline', (h) => COLON_HEADLINE.test(h)); + reportRuns(headlines, 'same leading connective', (h) => LEADING_CONNECTIVE.test(h)); + reportRuns(headlines, 'comma-pause two-part headline', (h) => + /^[^、]{2,14}、[^、]{1,10}$/.test(h), + ); +} + +function reportRuns(slides, name, test) { + let run = []; + const flush = () => { + if (run.length >= 3) console.log(`${name} x${run.length}: ${run.map(label).join(', ')}`); + run = []; + }; + for (const slide of slides) { + if (test(slide.headline)) run.push(slide); + else flush(); + } + flush(); +} + +// Full-width characters count 1, Latin letters and digits about half. +const LATIN_1_END = 0xff; +const HALF_WIDTH_KANA = [0xff61, 0xff9f]; +function displayWidth(text) { + let width = 0; + for (const character of text) { + const code = character.codePointAt(0); + width += + code <= LATIN_1_END || (code >= HALF_WIDTH_KANA[0] && code <= HALF_WIDTH_KANA[1]) ? 0.5 : 1; + } + return width; +} + +function median(values) { + const sorted = [...values].sort((a, b) => a - b); + return sorted[Math.floor(sorted.length / 2)]; +} + +// Full-width characters per line in a 16:9 title box at ~28pt; carnot-tech/consulting-pptx-skill §2.1. +const DOCUMENT_TITLE_LINE = 40; +const ELEMENT_COUNT = + /[0-90-9一二三四五六七八九十]+ ?(つ|個|種類|点|段階?|ステップ|フェーズ|論点|柱)(の|で|が|を|に|$)/; + +function printDocumentChecks(all) { + console.log('\n# Document headline checks'); + // The cover carries the talk title as given; it is not a headline to rewrite. + for (const slide of all.slice(1)) { + const headline = slide.headline; + if (headline === null) continue; + const issues = []; + if (POLITE_END.test(headline)) issues.push('polite ending (use plain form)'); + const lines = slide.headlineRaw.split(/[\n\v]/); + const widest = Math.max(...lines.map(displayWidth)); + if (lines.length > 2) issues.push(`${lines.length} lines (keep headlines to two)`); + else if (widest > DOCUMENT_TITLE_LINE * 2) issues.push('longer than two lines'); + else if (widest > DOCUMENT_TITLE_LINE) + issues.push('wraps without an explicit break (put one at a meaning boundary)'); + if (/^[^、。]{1,12}[::]/.test(headline)) issues.push('label prefix before a colon'); + if (/^(この|その|ここまで|これら)/.test(headline)) issues.push('refers to another slide'); + if (ELEMENT_COUNT.test(headline)) + issues.push('element count in headline (state the content, or check it matches the body)'); + if (issues.length) console.log(`${label(slide)} ${issues.join('; ')}: ${headline}`); + } +} + +// Each entry cites the rule in references/writing-ja.md. Order: highest confidence first. +const WORDING_PATTERNS = [ + // 効 inside 有効/効率/効果 is a different word. + ['効く', /(? text.split(/\n/))]; + const notes = slide.notes.split(/\n/); + for (const [where, line] of [ + ...lines.map((line) => ['', line]), + ...notes.map((line) => ['notes ', line]), + ]) { + const hits = flagsFor(line, where === ''); + if (hits.length === 0) continue; + count++; + console.log(`${label(slide)} ${where}[${hits.join(', ')}] ${oneLine(line).slice(0, 80)}`); + } + } + if (count === 0) console.log('none'); +} + +// Notes are spoken prose, so the slide-layout check (label: description) does not apply. +function flagsFor(line, onSlide) { + if (!line.trim()) return []; + const hits = WORDING_PATTERNS.filter(([, pattern]) => pattern.test(line)).map(([name]) => name); + if (onSlide && LABEL_COLON.test(line)) hits.push('label: description'); + if (FOREIGN_SCRIPT.test(line)) hits.push('foreign script'); + return hits; +} + +const slides = pptx.getSlides(presentation).map(readSlide); +printHeadlines(slides); +if (full) printFullText(slides); +if (mode === 'talk') printTalkMetrics(slides); +if (mode === 'document') printDocumentChecks(slides); +printWordingFlags(slides);