@hypersocial/decks-cli 0.4.0 → 0.5.0
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/package.json +1 -1
- package/skills/hyperdecks/SKILL.md +3 -0
- package/skills/hyperdecks/chat.md +23 -2
- package/src/commands.mjs +19 -0
- package/src/files.mjs +2 -0
- package/src/firstDeck.mjs +105 -0
- package/src/init.mjs +4 -101
- package/src/ops.mjs +62 -0
- package/src/runtime.mjs +7 -1
package/package.json
CHANGED
|
@@ -56,6 +56,9 @@ returns a link; hand it to them, word for word, and wait.
|
|
|
56
56
|
| Make one the default (the person named it) | `hyperdecks narrator default <name>` | `set_default_narrator` |
|
|
57
57
|
| Remove one (confirm first) | `hyperdecks narrator remove <name>` | `remove_narrator` |
|
|
58
58
|
| Change its voice (the person asked) | `hyperdecks narrator voice <name> <voice>` | `set_narrator_voice` |
|
|
59
|
+
| Rename it (the person asked) | `hyperdecks narrator rename <name> <new name>` | `rename_narrator` |
|
|
60
|
+
| Change its face to one of their avatars (the person named it) | `hyperdecks narrator face <name> <avatar>` | `set_narrator_face` |
|
|
61
|
+
| Hear a voice or a narrator | `hyperdecks voice hear <voice>` (`--narrator <name>`) → `url`, `caption` | `hear_voice` |
|
|
59
62
|
| One deck's own narrator | `hyperdecks deck narrator <slug> <name\|none\|default>` | `set_deck_narrator` |
|
|
60
63
|
|
|
61
64
|
### Avatars: who presents a deck in the user's voice
|
|
@@ -7,6 +7,18 @@ reaches a recipient until `release_deck`. Topics: call `deck_guide` with
|
|
|
7
7
|
`format`, `script`, `design`, `layouts`, `presenter`, `transitions` or `check`
|
|
8
8
|
before the step that needs it.
|
|
9
9
|
|
|
10
|
+
## First time here?
|
|
11
|
+
|
|
12
|
+
`get_started` with no answers says where the person stands (ready voices,
|
|
13
|
+
narrators, credits) and the steps. No voice of their own: `clone_voice_link`
|
|
14
|
+
(they record in the browser), or `clone_voice` with 1-3 recordings they attach
|
|
15
|
+
in the chat, only after they agree to the consent statement, word for word.
|
|
16
|
+
`hear_voice` plays a voice or a narrator in the chat. A face from a photo they
|
|
17
|
+
attach: `avatar_from_photo { file }`, then `check_avatar_draft` until it hands
|
|
18
|
+
back the link they approve it on. `rename_narrator` and `set_narrator_face`
|
|
19
|
+
change a narrator when they ask. Then `get_started { name, about, ask, voice }`
|
|
20
|
+
makes the hello deck; continue from step 5 below.
|
|
21
|
+
|
|
10
22
|
## The loop
|
|
11
23
|
|
|
12
24
|
1. `deck_guide` (this page), then `deck_guide { topic: "layouts" }` to pick slides.
|
|
@@ -31,8 +43,17 @@ before the step that needs it.
|
|
|
31
43
|
8. `release_deck { slug, qa: true }` gives the user a QA link to watch first.
|
|
32
44
|
`release_deck { slug, recipients: [{ recipient, email }] }` makes one
|
|
33
45
|
tracked link each. Release creates live links: confirm the recipients with
|
|
34
|
-
the user before you call it.
|
|
35
|
-
|
|
46
|
+
the user before you call it. In Claude and ChatGPT the result shows as a
|
|
47
|
+
deck card: the deck playing from its QA link (the draft before release),
|
|
48
|
+
with each share link and its opens. Never paste a recipient's link to "test"
|
|
49
|
+
it: opening it counts as their open.
|
|
50
|
+
9. Later: `list_shares` and `deck_stats` (both show the card),
|
|
51
|
+
`activity_overview`; `email_embed { token }` for the HTML that pastes into an
|
|
52
|
+
email as the deck's moving preview (it needs a recipient's link, not a QA
|
|
53
|
+
one); `set_share_notify` to mute a link's "opened" alerts;
|
|
54
|
+
`set_recipient_archived` to tidy the activity feed; `credits` for the
|
|
55
|
+
balance. `remove_deck` deletes a deck and breaks every link to it: confirm
|
|
56
|
+
with the user first.
|
|
36
57
|
|
|
37
58
|
## Rules that matter
|
|
38
59
|
|
package/src/commands.mjs
CHANGED
|
@@ -368,6 +368,8 @@ commands:
|
|
|
368
368
|
asks you to type "I consent" before anything is uploaded
|
|
369
369
|
voice upload <file...> upload recordings and print their sample ids (for the MCP clone_voice tool)
|
|
370
370
|
voice rm <name|id> delete a voice, also at the provider
|
|
371
|
+
voice hear <voice> | voice hear --narrator <name|id>
|
|
372
|
+
a short sample of the voice: its clip URL and the line it says
|
|
371
373
|
avatar [--json] your avatars (who presents decks in your voice) and the default
|
|
372
374
|
avatar new [--species <s>] pick a species, then an avatar (s shuffles); --json or an agent gets a pick link
|
|
373
375
|
avatar from-photo [file] [--save-to-minimoji]
|
|
@@ -381,6 +383,8 @@ commands:
|
|
|
381
383
|
narrator default <name|id|dash> make it the default
|
|
382
384
|
narrator remove <name|id> remove it; decks already shared keep working
|
|
383
385
|
narrator voice <name|id> <voice> change its voice (one of yours, or dash)
|
|
386
|
+
narrator rename <name|id> <new name> rename it
|
|
387
|
+
narrator face <name|id> <avatar> change its face (one of your avatars)
|
|
384
388
|
deck narrator <slug> <name|id|none|default> the deck's own narrator (none: captions only)
|
|
385
389
|
|
|
386
390
|
global:
|
|
@@ -766,6 +770,21 @@ function commandTable(pos, rest, opts) {
|
|
|
766
770
|
const out = await ops.setNarratorVoice(opts, rest[0], rest.slice(1).join(" "));
|
|
767
771
|
console.log(opts.json ? JSON.stringify(out, null, 2) : `OK ${out.name} speaks in ${out.voice} now`);
|
|
768
772
|
},
|
|
773
|
+
"narrator rename": async () => {
|
|
774
|
+
if (rest.length < 2) throw new Error("usage: narrator rename <name|id> <new name>");
|
|
775
|
+
const out = await ops.renameNarrator(opts, rest[0], rest.slice(1).join(" "));
|
|
776
|
+
console.log(opts.json ? JSON.stringify(out, null, 2) : `OK ${out.was} is ${out.name} now`);
|
|
777
|
+
},
|
|
778
|
+
"narrator face": async () => {
|
|
779
|
+
if (rest.length < 2) throw new Error("usage: narrator face <name|id> <avatar>");
|
|
780
|
+
const out = await ops.setNarratorFace(opts, rest[0], rest.slice(1).join(" "));
|
|
781
|
+
console.log(opts.json ? JSON.stringify(out, null, 2) : `OK ${out.name} wears ${out.face} now`);
|
|
782
|
+
},
|
|
783
|
+
"voice hear": async () => {
|
|
784
|
+
if (!rest[0] && !opts.narrator) throw new Error("usage: voice hear <voice> | voice hear --narrator <name|id>");
|
|
785
|
+
const out = await ops.hearVoice(opts, opts.narrator ? { narrator: opts.narrator } : { voice: rest.join(" ") });
|
|
786
|
+
console.log(opts.json ? JSON.stringify(out, null, 2) : `"${out.caption}"\n ${out.url}`);
|
|
787
|
+
},
|
|
769
788
|
"deck narrator": async () => {
|
|
770
789
|
if (rest.length < 2) throw new Error("usage: deck narrator <slug> <name|id|none|default>");
|
|
771
790
|
const out = await ops.setDeckNarrator(opts, rest[0], rest.slice(1).join(" "));
|
package/src/files.mjs
CHANGED
|
@@ -19,6 +19,8 @@ const FILES = {
|
|
|
19
19
|
"templates/layouts/quote.html": () => new URL("../templates/layouts/quote.html", import.meta.url),
|
|
20
20
|
"templates/layouts/team.html": () => new URL("../templates/layouts/team.html", import.meta.url),
|
|
21
21
|
"templates/layouts/timeline.html": () => new URL("../templates/layouts/timeline.html", import.meta.url),
|
|
22
|
+
"templates/first-deck/self.json": () => new URL("../templates/first-deck/self.json", import.meta.url),
|
|
23
|
+
"templates/first-deck/character.json": () => new URL("../templates/first-deck/character.json", import.meta.url),
|
|
22
24
|
"skills/hyperdecks/SKILL.md": () => new URL("../skills/hyperdecks/SKILL.md", import.meta.url),
|
|
23
25
|
"skills/hyperdecks/chat.md": () => new URL("../skills/hyperdecks/chat.md", import.meta.url),
|
|
24
26
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The first deck `hyperdecks init` (and the hosted get_started tool) makes: a
|
|
2
|
+
// three-slide hello deck (hero, features, closer) filled from three answers.
|
|
3
|
+
// No file or process I/O beyond the packaged templates, so the hosted MCP route
|
|
4
|
+
// can bundle it.
|
|
5
|
+
import { packageText } from "./files.mjs";
|
|
6
|
+
import { slugify } from "./importer.mjs";
|
|
7
|
+
|
|
8
|
+
export const FIRST_DECK_LAYOUTS = "hero,features,closer";
|
|
9
|
+
export const DASH_AVATAR = "https://decks.hypersocial.ai/player/v1/dash-avatar.json";
|
|
10
|
+
export const ANSWER_MAX = { name: 40, about: 80, ask: 80 };
|
|
11
|
+
|
|
12
|
+
// One answer as it goes on a slide and into the script: one line, no stage-tag
|
|
13
|
+
// brackets, ending in a full stop so it is its own caption.
|
|
14
|
+
export function cleanAnswer(key, value) {
|
|
15
|
+
const text = String(value ?? "").replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
|
|
16
|
+
if (text.length < 2) throw new Error(`--${key} needs an answer`);
|
|
17
|
+
if (text.length > ANSWER_MAX[key]) throw new Error(`--${key} is ${text.length} characters; keep it to ${ANSWER_MAX[key]} so it fits a caption`);
|
|
18
|
+
return key === "name" ? text.replace(/[.!?]+$/, "") : /[.!?…]$/.test(text) ? text : `${text}.`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const firstDeckSlug = (email) => `hello-${slugify(String(email ?? "").split("@")[0]) || "there"}`.slice(0, 58).replace(/-+$/, "");
|
|
22
|
+
|
|
23
|
+
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
24
|
+
|
|
25
|
+
export function firstDeckCopy(presenter) {
|
|
26
|
+
return JSON.parse(packageText(`templates/first-deck/${presenter === "dash" ? "character" : "self"}.json`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function slots(text, answers, html) {
|
|
30
|
+
const map = { __NAME__: answers.name, __WHAT__: answers.about, __ASK__: answers.ask };
|
|
31
|
+
return text.replace(/__NAME__|__WHAT__|__ASK__/g, (k) => (html ? escapeHtml(map[k]) : map[k]));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Replace exactly one match, or fail loudly: a layout change must not ship a deck
|
|
35
|
+
// that still says "One sentence on what this deck is about."
|
|
36
|
+
function swap(html, pattern, to, what) {
|
|
37
|
+
let hit = 0;
|
|
38
|
+
const out = html.replace(pattern, (...m) => (hit++, typeof to === "function" ? to(...m) : to));
|
|
39
|
+
if (hit !== 1) throw new Error(`first deck: the ${what} in the layout template changed (${hit} matches); update init's first-deck fill`);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function section(html, id, edit) {
|
|
44
|
+
return swap(html, new RegExp(`<section\\b[^>]*\\sid="${id}"[\\s\\S]*?</section>`), (s) => edit(s), `#${id} slide`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// index.html and script.md of the scaffolded hero,features,closer deck, filled
|
|
48
|
+
// with the answers: no template copy is left, and no generated imagery.
|
|
49
|
+
export function fillFirstDeck({ html, title, answers, presenter, voice }) {
|
|
50
|
+
const copy = firstDeckCopy(presenter);
|
|
51
|
+
const h = (t) => slots(t, answers, true);
|
|
52
|
+
const note = (s) => swap(s, /\sdata-note="[^"]*"/, ' data-note="From your init answers."', "data-note");
|
|
53
|
+
let out = section(html, "hero", (s) => {
|
|
54
|
+
s = note(s);
|
|
55
|
+
s = swap(s, /<div class="kicker">[^<]*<\/div>/, `<div class="kicker">${h(copy.hero.kicker)}</div>`, "hero kicker");
|
|
56
|
+
s = swap(s, /<h1 class="tt"[^>]*>[\s\S]*?<\/h1>/, `<h1 class="tt" data-title-mobile="${h(copy.hero.title)}">${h(copy.hero.title)}</h1>`, "hero title");
|
|
57
|
+
return swap(s, /<p class="lead">[\s\S]*?<\/p>/, `<p class="lead">${h(copy.hero.lead)}</p>`, "hero lead");
|
|
58
|
+
});
|
|
59
|
+
out = section(out, "features", (s) => {
|
|
60
|
+
s = note(s);
|
|
61
|
+
s = swap(s, /<div class="kicker">[^<]*<\/div>/, `<div class="kicker">${h(copy.features.kicker)}</div>`, "features kicker");
|
|
62
|
+
s = swap(s, /<h2 class="tt"[^>]*>[\s\S]*?<\/h2>/, `<h2 class="tt" data-title-mobile="${h(copy.features.title)}">${h(copy.features.title)}</h2>`, "features title");
|
|
63
|
+
copy.features.cards.forEach((card, i) => {
|
|
64
|
+
s = swap(s, new RegExp(`(<li class="card" id="features-f${i + 1}">[\\s\\S]*?</svg>)<b>[^<]*</b><p>[^<]*</p>`), (_, icon) => `${icon}<b>${h(card.name)}</b><p>${h(card.line)}</p>`, `features card ${i + 1}`);
|
|
65
|
+
});
|
|
66
|
+
return s;
|
|
67
|
+
});
|
|
68
|
+
out = section(out, "closer", (s) => {
|
|
69
|
+
s = note(s);
|
|
70
|
+
s = swap(s, /<h2 class="tt"[^>]*>[\s\S]*?<\/h2>/, `<h2 class="tt" data-title-mobile="${h(copy.closer.title)}">${h(copy.closer.title)}</h2>`, "closer title");
|
|
71
|
+
s = swap(s, /<p class="lead">[\s\S]*?<\/p>/, `<p class="lead">${h(copy.closer.lead)}</p>`, "closer lead");
|
|
72
|
+
s = swap(s, /<a class="cta"[^>]*>[\s\S]*?<\/a>/, `<p class="cta" id="closer-cta">${h(copy.closer.cta)}</p>`, "closer call to action");
|
|
73
|
+
return swap(s, /<div class="contact muted">[^<]*<\/div>/, `<div class="contact muted">${h(copy.closer.contact)}</div>`, "closer contact");
|
|
74
|
+
});
|
|
75
|
+
// A character's live head presents its own deck; a person's voice ships with
|
|
76
|
+
// captions only (data-presenter="none"), never on a character's face.
|
|
77
|
+
if (presenter === "dash") {
|
|
78
|
+
out = swap(out, /(<script type="module" src="https:\/\/decks\.hypersocial\.ai\/player\/v1\/player\.js"[^>]*?)\sdata-presenter="none"/, (_, tag) => `${tag} data-presenter="head" data-avatar="${DASH_AVATAR}"`, "player tag");
|
|
79
|
+
}
|
|
80
|
+
const s = (t) => slots(t, answers, false);
|
|
81
|
+
const script = `---
|
|
82
|
+
voice: ${voice}
|
|
83
|
+
maxTotalSeconds: 90
|
|
84
|
+
transition: drift
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# ${title} — narration script
|
|
88
|
+
|
|
89
|
+
Written by \`hyperdecks init\` from your three answers. Make it yours: rewrite
|
|
90
|
+
these lines and the slides in index.html, then
|
|
91
|
+
|
|
92
|
+
hyperdecks narrate .
|
|
93
|
+
hyperdecks release . --to "<name>"
|
|
94
|
+
|
|
95
|
+
## hero
|
|
96
|
+
${s(copy.hero.script)}
|
|
97
|
+
|
|
98
|
+
## features
|
|
99
|
+
${s(copy.features.script)}
|
|
100
|
+
|
|
101
|
+
## closer
|
|
102
|
+
${s(copy.closer.script)}
|
|
103
|
+
`;
|
|
104
|
+
return { html: out, script };
|
|
105
|
+
}
|
package/src/init.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// QA link and print it. Every step checks real state before it acts, so running
|
|
7
7
|
// init again resumes where it stopped and changes nothing once it is complete.
|
|
8
8
|
// Each step returns { step, status: "done" | "skipped" | "ran" | "needs-human", detail }.
|
|
9
|
-
//
|
|
9
|
+
// In chat (no folder, no git) the hosted MCP's get_started tool walks the same
|
|
10
|
+
// steps on a hosted draft (lib/getStarted.ts); the deck fill is shared (firstDeck.mjs).
|
|
10
11
|
import { execFileSync } from "node:child_process";
|
|
11
12
|
import { createHash, randomBytes } from "node:crypto";
|
|
12
13
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -15,7 +16,7 @@ import { dirname, join, relative, resolve } from "node:path";
|
|
|
15
16
|
import { createInterface } from "node:readline/promises";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
17
18
|
import * as ops from "./ops.mjs";
|
|
18
|
-
import {
|
|
19
|
+
import { ANSWER_MAX, DASH_AVATAR, FIRST_DECK_LAYOUTS, cleanAnswer, fillFirstDeck, firstDeckCopy, firstDeckSlug } from "./firstDeck.mjs";
|
|
19
20
|
import { agentTable, api, chooseAgents, client, CRED_DIR, installHyperdecksSkill, login as runLogin, openBrowser, target } from "./runtime.mjs";
|
|
20
21
|
import { READING_PASSAGE, voiceClone as runVoiceClone } from "./voice.mjs";
|
|
21
22
|
import { assembleLayoutDeck, deckNew as runDeckNew } from "./scaffold.mjs";
|
|
@@ -23,17 +24,13 @@ import { narrate as runNarrate } from "./narrate.mjs";
|
|
|
23
24
|
import { release as runRelease } from "./release.mjs";
|
|
24
25
|
|
|
25
26
|
export const STATE_FILE = join(CRED_DIR, "init.json");
|
|
26
|
-
export const FIRST_DECK_LAYOUTS = "hero,features,closer";
|
|
27
|
-
export const DASH_AVATAR = "https://decks.hypersocial.ai/player/v1/dash-avatar.json";
|
|
28
27
|
// Same numbers as convex/lib/tokenCost.ts (the published CLI cannot import it).
|
|
29
28
|
export const VOICE_CLONE_COST = 10;
|
|
30
29
|
export const STARTER_GRANT = 30;
|
|
31
|
-
export const ANSWER_MAX = { name: 40, about: 80, ask: 80 };
|
|
32
30
|
const SKILL_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "skills", "hyperdecks");
|
|
33
|
-
// CLI-only, so read next to the package rather than through files.mjs (the hosted route's list).
|
|
34
|
-
const FIRST_DECK_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates", "first-deck");
|
|
35
31
|
const CHARACTERS = ["dash", "d.a.s.h."];
|
|
36
32
|
const RESUME = "npx hyperdecks init";
|
|
33
|
+
export { ANSWER_MAX, DASH_AVATAR, FIRST_DECK_LAYOUTS, cleanAnswer, fillFirstDeck, firstDeckCopy, firstDeckSlug };
|
|
37
34
|
export const AGENT_VOICE_HELP = "ask the user to run `npx hyperdecks voice clone <file> --name <name>` in a terminal, " +
|
|
38
35
|
"or upload with `voice upload` and call `clone_voice` after they agree to the statement; " +
|
|
39
36
|
"or run init again with `--voice dash` to let D.A.S.H. present";
|
|
@@ -66,100 +63,6 @@ export function writeState(path, all) {
|
|
|
66
63
|
|
|
67
64
|
// ---- answers and the first deck -------------------------------------------
|
|
68
65
|
|
|
69
|
-
// One answer as it goes on a slide and into the script: one line, no stage-tag
|
|
70
|
-
// brackets, ending in a full stop so it is its own caption.
|
|
71
|
-
export function cleanAnswer(key, value) {
|
|
72
|
-
const text = String(value ?? "").replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
|
|
73
|
-
if (text.length < 2) throw new Error(`--${key} needs an answer`);
|
|
74
|
-
if (text.length > ANSWER_MAX[key]) throw new Error(`--${key} is ${text.length} characters; keep it to ${ANSWER_MAX[key]} so it fits a caption`);
|
|
75
|
-
return key === "name" ? text.replace(/[.!?]+$/, "") : /[.!?…]$/.test(text) ? text : `${text}.`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export const firstDeckSlug = (email) => `hello-${slugify(String(email ?? "").split("@")[0]) || "there"}`.slice(0, 58).replace(/-+$/, "");
|
|
79
|
-
|
|
80
|
-
const escapeHtml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
81
|
-
|
|
82
|
-
export function firstDeckCopy(presenter) {
|
|
83
|
-
return JSON.parse(readFileSync(join(FIRST_DECK_DIR, `${presenter === "dash" ? "character" : "self"}.json`), "utf8"));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function slots(text, answers, html) {
|
|
87
|
-
const map = { __NAME__: answers.name, __WHAT__: answers.about, __ASK__: answers.ask };
|
|
88
|
-
return text.replace(/__NAME__|__WHAT__|__ASK__/g, (k) => (html ? escapeHtml(map[k]) : map[k]));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Replace exactly one match, or fail loudly: a layout change must not ship a deck
|
|
92
|
-
// that still says "One sentence on what this deck is about."
|
|
93
|
-
function swap(html, pattern, to, what) {
|
|
94
|
-
let hit = 0;
|
|
95
|
-
const out = html.replace(pattern, (...m) => (hit++, typeof to === "function" ? to(...m) : to));
|
|
96
|
-
if (hit !== 1) throw new Error(`first deck: the ${what} in the layout template changed (${hit} matches); update init's first-deck fill`);
|
|
97
|
-
return out;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function section(html, id, edit) {
|
|
101
|
-
return swap(html, new RegExp(`<section\\b[^>]*\\sid="${id}"[\\s\\S]*?</section>`), (s) => edit(s), `#${id} slide`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// index.html and script.md of the scaffolded hero,features,closer deck, filled
|
|
105
|
-
// with the answers: no template copy is left, and no generated imagery.
|
|
106
|
-
export function fillFirstDeck({ html, title, answers, presenter, voice }) {
|
|
107
|
-
const copy = firstDeckCopy(presenter);
|
|
108
|
-
const h = (t) => slots(t, answers, true);
|
|
109
|
-
const note = (s) => swap(s, /\sdata-note="[^"]*"/, ' data-note="From your init answers."', "data-note");
|
|
110
|
-
let out = section(html, "hero", (s) => {
|
|
111
|
-
s = note(s);
|
|
112
|
-
s = swap(s, /<div class="kicker">[^<]*<\/div>/, `<div class="kicker">${h(copy.hero.kicker)}</div>`, "hero kicker");
|
|
113
|
-
s = swap(s, /<h1 class="tt"[^>]*>[\s\S]*?<\/h1>/, `<h1 class="tt" data-title-mobile="${h(copy.hero.title)}">${h(copy.hero.title)}</h1>`, "hero title");
|
|
114
|
-
return swap(s, /<p class="lead">[\s\S]*?<\/p>/, `<p class="lead">${h(copy.hero.lead)}</p>`, "hero lead");
|
|
115
|
-
});
|
|
116
|
-
out = section(out, "features", (s) => {
|
|
117
|
-
s = note(s);
|
|
118
|
-
s = swap(s, /<div class="kicker">[^<]*<\/div>/, `<div class="kicker">${h(copy.features.kicker)}</div>`, "features kicker");
|
|
119
|
-
s = swap(s, /<h2 class="tt"[^>]*>[\s\S]*?<\/h2>/, `<h2 class="tt" data-title-mobile="${h(copy.features.title)}">${h(copy.features.title)}</h2>`, "features title");
|
|
120
|
-
copy.features.cards.forEach((card, i) => {
|
|
121
|
-
s = swap(s, new RegExp(`(<li class="card" id="features-f${i + 1}">[\\s\\S]*?</svg>)<b>[^<]*</b><p>[^<]*</p>`), (_, icon) => `${icon}<b>${h(card.name)}</b><p>${h(card.line)}</p>`, `features card ${i + 1}`);
|
|
122
|
-
});
|
|
123
|
-
return s;
|
|
124
|
-
});
|
|
125
|
-
out = section(out, "closer", (s) => {
|
|
126
|
-
s = note(s);
|
|
127
|
-
s = swap(s, /<h2 class="tt"[^>]*>[\s\S]*?<\/h2>/, `<h2 class="tt" data-title-mobile="${h(copy.closer.title)}">${h(copy.closer.title)}</h2>`, "closer title");
|
|
128
|
-
s = swap(s, /<p class="lead">[\s\S]*?<\/p>/, `<p class="lead">${h(copy.closer.lead)}</p>`, "closer lead");
|
|
129
|
-
s = swap(s, /<a class="cta"[^>]*>[\s\S]*?<\/a>/, `<p class="cta" id="closer-cta">${h(copy.closer.cta)}</p>`, "closer call to action");
|
|
130
|
-
return swap(s, /<div class="contact muted">[^<]*<\/div>/, `<div class="contact muted">${h(copy.closer.contact)}</div>`, "closer contact");
|
|
131
|
-
});
|
|
132
|
-
// A character's live head presents its own deck; a person's voice ships with
|
|
133
|
-
// captions only (data-presenter="none"), never on a character's face.
|
|
134
|
-
if (presenter === "dash") {
|
|
135
|
-
out = swap(out, /(<script type="module" src="https:\/\/decks\.hypersocial\.ai\/player\/v1\/player\.js"[^>]*?)\sdata-presenter="none"/, (_, tag) => `${tag} data-presenter="head" data-avatar="${DASH_AVATAR}"`, "player tag");
|
|
136
|
-
}
|
|
137
|
-
const s = (t) => slots(t, answers, false);
|
|
138
|
-
const script = `---
|
|
139
|
-
voice: ${voice}
|
|
140
|
-
maxTotalSeconds: 90
|
|
141
|
-
transition: drift
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
# ${title} — narration script
|
|
145
|
-
|
|
146
|
-
Written by \`hyperdecks init\` from your three answers. Make it yours: rewrite
|
|
147
|
-
these lines and the slides in index.html, then
|
|
148
|
-
|
|
149
|
-
hyperdecks narrate .
|
|
150
|
-
hyperdecks release . --to "<name>"
|
|
151
|
-
|
|
152
|
-
## hero
|
|
153
|
-
${s(copy.hero.script)}
|
|
154
|
-
|
|
155
|
-
## features
|
|
156
|
-
${s(copy.features.script)}
|
|
157
|
-
|
|
158
|
-
## closer
|
|
159
|
-
${s(copy.closer.script)}
|
|
160
|
-
`;
|
|
161
|
-
return { html: out, script };
|
|
162
|
-
}
|
|
163
66
|
|
|
164
67
|
// ---- git ------------------------------------------------------------------
|
|
165
68
|
|
package/src/ops.mjs
CHANGED
|
@@ -1019,6 +1019,68 @@ export async function setNarratorVoice(opts, ref, voice) {
|
|
|
1019
1019
|
return { narrator: n.id, name: n.name, voice: String(voice) };
|
|
1020
1020
|
}
|
|
1021
1021
|
|
|
1022
|
+
export async function renameNarrator(opts, ref, name) {
|
|
1023
|
+
if (!String(name ?? "").trim()) throw new Error("usage: narrator rename <name|id> <new name>");
|
|
1024
|
+
const n = await findNarrator(opts, ref);
|
|
1025
|
+
if (n.builtin) throw new Error("D.A.S.H. is built in; its name stays");
|
|
1026
|
+
await (await client(opts)).mutation(api.narrators.rename, { narrator: n.id, name: String(name).trim() });
|
|
1027
|
+
return { narrator: n.id, was: n.name, name: String(name).trim() };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// face: one of your avatars (name or id from `avatar`); D.A.S.H. keeps its own.
|
|
1031
|
+
export async function setNarratorFace(opts, ref, avatarRef) {
|
|
1032
|
+
if (!avatarRef) throw new Error("usage: narrator face <name|id> <avatar>");
|
|
1033
|
+
const n = await findNarrator(opts, ref);
|
|
1034
|
+
if (n.builtin) throw new Error("D.A.S.H. is built in and keeps its face");
|
|
1035
|
+
const a = await findAvatar(opts, avatarRef);
|
|
1036
|
+
if (a.source === "dash") throw new Error("D.A.S.H.'s face is D.A.S.H.'s; pick one of your avatars");
|
|
1037
|
+
await (await client(opts)).mutation(api.narrators.setFace, { narrator: n.id, avatarId: a.id });
|
|
1038
|
+
return { narrator: n.id, name: n.name, face: a.name };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// A short sample of a voice, or of a narrator's voice: { url, caption } (what
|
|
1042
|
+
// the dashboard's play buttons play). A narrator also brings its face still.
|
|
1043
|
+
export async function hearVoice(opts, { voice, narrator } = {}) {
|
|
1044
|
+
const c = await client(opts);
|
|
1045
|
+
if (!voice && !narrator) throw new Error("name a voice or a narrator (list_voices / list_narrators)");
|
|
1046
|
+
let ref = voice ? String(voice) : null;
|
|
1047
|
+
let who = null;
|
|
1048
|
+
if (narrator) {
|
|
1049
|
+
const n = await findNarrator(opts, narrator);
|
|
1050
|
+
const raw = (await c.query(api.narrators.list, {})).narrators.find((x) => x.id === n.id);
|
|
1051
|
+
if (!raw?.voice) throw new Error(`${n.name} has no voice yet; give it one (narrator voice)`);
|
|
1052
|
+
if (!raw.voice.ready) throw new Error(`${n.name}'s voice ${raw.voice.name} is not ready yet`);
|
|
1053
|
+
ref = raw.voice.ref;
|
|
1054
|
+
who = { narrator: n.id, name: n.name, faceUrl: raw.face?.thumbUrl ?? null, faceName: raw.face?.name ?? null };
|
|
1055
|
+
}
|
|
1056
|
+
const out = await c.action(api.narratorsNode.hear, { voice: ref });
|
|
1057
|
+
return { ...out, voice: ref, ...(who ?? {}) };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// A photo drawing started from chat (avatar_from_photo with a photo): still
|
|
1061
|
+
// drawing, failed, or drawn, when it hands back the link where the person
|
|
1062
|
+
// approves it. Nothing is used before they do.
|
|
1063
|
+
export async function startAvatarDrawing(opts, { bytes, contentType }) {
|
|
1064
|
+
if (!/^image\/(jpeg|png)$/.test(contentType ?? "")) throw new Error("the photo must be a .jpg or .png");
|
|
1065
|
+
const c = await client(opts);
|
|
1066
|
+
const storageId = await runtimeUploadBytes(c, bytes, contentType);
|
|
1067
|
+
const { draftId } = await c.mutation(api.avatars.drawSelfie, { storageId });
|
|
1068
|
+
return { status: "drawing", draftId };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export async function avatarDraftStatus(opts, draftId) {
|
|
1072
|
+
if (!draftId) throw new Error("draftId is required");
|
|
1073
|
+
const c = await client(opts);
|
|
1074
|
+
const draft = await c.query(api.avatars.getDraft, { draftId: String(draftId) });
|
|
1075
|
+
if (!draft) throw new Error("the drawing is gone; start again with avatar_from_photo");
|
|
1076
|
+
if (draft.status === "drawing") return { status: "drawing", draftId };
|
|
1077
|
+
if (draft.status === "failed") throw new Error(draft.error || "the drawing failed; try another photo");
|
|
1078
|
+
if (draft.status === "added") return { status: "added", draftId, avatarId: draft.avatarId ?? null };
|
|
1079
|
+
if (draft.status !== "drawn") throw new Error(`the drawing is in an unexpected state (${draft.status}); start again with avatar_from_photo`);
|
|
1080
|
+
const link = await c.mutation(api.avatars.createLink, { kind: "review", draftId });
|
|
1081
|
+
return { status: "waiting", reviewUrl: link.url, token: link.token, draftId, expiresAt: link.expiresAt };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1022
1084
|
// A deck's own narrator: a narrator, `none` (captions only) or `default`.
|
|
1023
1085
|
export async function setDeckNarrator(opts, slug, ref) {
|
|
1024
1086
|
if (!slug || !ref) throw new Error("usage: deck narrator <slug> <narrator|none|default>");
|
package/src/runtime.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { ConvexHttpClient } from "convex/browser";
|
|
9
9
|
import { makeFunctionReference } from "convex/server";
|
|
10
10
|
|
|
11
|
-
export const VERSION = "0.
|
|
11
|
+
export const VERSION = "0.5.0";
|
|
12
12
|
const UPLOAD_TIMEOUT_MS = 125_000;
|
|
13
13
|
|
|
14
14
|
export const PROD = {
|
|
@@ -40,6 +40,11 @@ export const api = {
|
|
|
40
40
|
setVoice: makeFunctionReference("narrators:setVoice"),
|
|
41
41
|
setDeckNarrator: makeFunctionReference("narrators:setDeckNarrator"),
|
|
42
42
|
newLink: makeFunctionReference("narrators:newLink"),
|
|
43
|
+
rename: makeFunctionReference("narrators:rename"),
|
|
44
|
+
setFace: makeFunctionReference("narrators:setFace"),
|
|
45
|
+
},
|
|
46
|
+
narratorsNode: {
|
|
47
|
+
hear: makeFunctionReference("narratorsNode:hear"),
|
|
43
48
|
},
|
|
44
49
|
avatarsNode: {
|
|
45
50
|
options: makeFunctionReference("avatarsNode:options"),
|
|
@@ -49,6 +54,7 @@ export const api = {
|
|
|
49
54
|
assets: {
|
|
50
55
|
generateUploadUrl: makeFunctionReference("assets:generateUploadUrl"),
|
|
51
56
|
saveAsset: makeFunctionReference("assets:saveAsset"),
|
|
57
|
+
list: makeFunctionReference("assets:list"),
|
|
52
58
|
},
|
|
53
59
|
assetsNode: {
|
|
54
60
|
importAssetFromUrl: makeFunctionReference("assetsNode:importAssetFromUrl"),
|