defiant-jazz 0.3.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 kenjonespizza
3
+ Copyright (c) 2026 pelikenjones
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,83 +1,117 @@
1
1
  # defiant-jazz
2
2
 
3
- Transform quotes into Severance character voices using OpenAI.
4
-
5
- ## Installation
3
+ Transform quotes into Severance character voices, using Claude or OpenAI.
6
4
 
7
5
  ```bash
8
6
  npm install defiant-jazz
9
7
  ```
10
8
 
11
- ## Setup
12
-
13
- Set your OpenAI API key as an environment variable:
14
-
15
- ```bash
16
- export OPENAI_API_KEY=sk-...
17
- ```
18
-
19
- Or configure it in code:
20
-
21
- ```typescript
22
- import { configure } from 'defiant-jazz';
23
-
24
- configure({ apiKey: 'sk-...' });
25
- ```
26
-
27
- > **Security note**: Never hardcode API keys in your source code. Use environment variables or a secrets manager.
28
-
29
9
  ## Usage
30
10
 
31
11
  ```typescript
32
12
  import { refine } from 'defiant-jazz';
33
13
 
34
- // Transform a quote using character methods
35
14
  const result = await refine.dylan("I need to focus on my work");
36
15
  console.log(result.text);
37
- // => "Oh great, more 'work.' Can't wait to stare at numbers until my brain leaks out my ears."
16
+ // => "Yeah, no, I gotta get back to my numbers. Some of us are actually
17
+ // trying to hit quota here, so — shoo. Go bother someone in O&D."
38
18
  console.log(result.character);
39
19
  // => "Dylan G."
40
20
 
41
- // Available characters
42
21
  await refine.mark("..."); // Mark S. - thoughtful, restrained
43
22
  await refine.irving("..."); // Irving B. - formal, poetic
44
23
  await refine.dylan("..."); // Dylan G. - sarcastic, irreverent
45
24
  await refine.milchick("..."); // Mr. Milchick - cheerful, corporate
46
25
 
47
- // Generic form (useful for dynamic character selection)
26
+ // Generic form, useful for dynamic character selection
48
27
  await refine("Hello world", "dylan");
49
28
  ```
50
29
 
51
- ## Configuration
30
+ Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` in your environment and you're done —
31
+ whichever is present is used automatically (Anthropic first if both are set).
32
+
33
+ **Default models: `claude-sonnet-5` (Anthropic), `gpt-4o-mini` (OpenAI).**
34
+ Sonnet 5 was chosen over Opus 5 after a head-to-head comparison found
35
+ matching output quality at 40% of the cost — see
36
+ [model-comparison.md](model-comparison.md) for the full writeup, including a
37
+ documented regression on Haiku 4.5 that ruled it out as a default. Override
38
+ with `model` on `createDefiantJazz()` or per call; see
39
+ [Choosing a provider](#choosing-a-provider).
40
+
41
+ ## Lore
42
+
43
+ By default, characters reach for Lumon/*Severance* vocabulary — the break
44
+ room, waffle parties, Kier, innie/outie — when a quote's situation actually
45
+ calls for it. It's restrained on purpose: usually zero references, at most
46
+ one, and only when it genuinely fits. Same quote, both settings:
52
47
 
53
48
  ```typescript
54
- import { configure } from 'defiant-jazz';
49
+ await refine.dylan("They gave us pizza instead of raises");
50
+ // => "Pizza. Not money — *pizza*. A greasy little circle of bread and
51
+ // suddenly we're all supposed to be jazzed about it. Cool. Great.
52
+ // Love that math.
53
+ //
54
+ // Although… okay, real talk, was it the good kind? Because if
55
+ // somebody's putting stuffed crust in front of me, I'm compromised.
56
+ // I'm not proud of it. I'd sell you out for a waffle party too, so."
57
+
58
+ await refine.dylan("They gave us pizza instead of raises", { lore: false });
59
+ // => "Oh cool, no raise, but hey — pizza. Cheese and marinara, the
60
+ // universal language of 'go screw yourself.' You know what I can't
61
+ // do with a pepperoni? Pay rent. But sure, throw me a slice, I'll
62
+ // frame it. Employee of the goddamn month."
63
+ ```
64
+
65
+ Turn it off per-call, or for a whole instance:
55
66
 
56
- configure({
57
- apiKey: 'sk-...', // Required if OPENAI_API_KEY not set
58
- model: 'gpt-4o' // Optional, defaults to 'gpt-4o-mini'
67
+ ```typescript
68
+ await refine.dylan("...", { lore: false });
69
+
70
+ const plain = createDefiantJazz({ lore: false });
71
+ ```
72
+
73
+ ## Choosing a provider
74
+
75
+ ```typescript
76
+ import { createDefiantJazz } from 'defiant-jazz';
77
+
78
+ const claude = createDefiantJazz({
79
+ provider: 'anthropic',
80
+ apiKey: 'sk-ant-...',
81
+ model: 'claude-sonnet-5', // optional, this is the default — see model-comparison.md
82
+ });
83
+
84
+ const gpt = createDefiantJazz({
85
+ provider: 'openai',
86
+ apiKey: 'sk-...',
87
+ model: 'gpt-4o-mini', // optional, this is the default
59
88
  });
89
+
90
+ await claude.dylan("Hello world");
91
+ await gpt.dylan("Hello world");
60
92
  ```
61
93
 
62
- ## Response
94
+ `refine` is a shared default instance using auto-detected credentials. Use
95
+ `createDefiantJazz()` whenever you need an isolated instance — a specific
96
+ provider, different API keys per request, a non-default model, testing.
97
+
98
+ Per-call overrides work on both forms:
63
99
 
64
100
  ```typescript
65
- interface RefineResult {
66
- text: string; // The transformed quote
67
- character: string; // Character name (e.g. "Dylan G.")
68
- }
101
+ await refine.dylan("Hello world", { model: 'claude-opus-5', maxTokens: 1024 });
69
102
  ```
70
103
 
71
104
  ## Characters
72
105
 
73
- Access the character map for building UIs or iterating:
74
-
75
106
  ```typescript
76
107
  import { CHARACTERS } from 'defiant-jazz';
77
108
 
78
109
  // { mark: "Mark S.", irving: "Irving B.", dylan: "Dylan G.", milchick: "Mr. Milchick" }
79
-
80
- Object.entries(CHARACTERS).map(([key, name]) => (
81
- <option value={key}>{name}</option>
82
- ));
83
110
  ```
111
+
112
+ Transformations draw on Lumon/*Severance* vocabulary by default — see
113
+ [Lore](#lore) above for what that means and how to turn it off.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { CharacterKey, DefiantJazzOptions, RefineOptions, RefineResult } from "./types.js";
2
+ export interface DefiantJazz {
3
+ (text: string, character: CharacterKey, options?: RefineOptions): Promise<RefineResult>;
4
+ mark: (text: string, options?: RefineOptions) => Promise<RefineResult>;
5
+ irving: (text: string, options?: RefineOptions) => Promise<RefineResult>;
6
+ dylan: (text: string, options?: RefineOptions) => Promise<RefineResult>;
7
+ milchick: (text: string, options?: RefineOptions) => Promise<RefineResult>;
8
+ configure: (options: DefiantJazzOptions) => void;
9
+ }
10
+ export declare function createDefiantJazz(initialOptions?: DefiantJazzOptions): DefiantJazz;
package/dist/client.js ADDED
@@ -0,0 +1,73 @@
1
+ import { createAnthropicClient, runAnthropic } from "./providers/anthropic.js";
2
+ import { createOpenAiClient, runOpenAi } from "./providers/openai.js";
3
+ import { buildSystemPrompt, CHARACTERS } from "./prompt.js";
4
+ function resolveProvider(options) {
5
+ if (options.provider === "anthropic" || options.provider === "openai") {
6
+ const apiKey = options.apiKey ??
7
+ (options.provider === "anthropic"
8
+ ? process.env.ANTHROPIC_API_KEY
9
+ : process.env.OPENAI_API_KEY);
10
+ if (!apiKey) {
11
+ throw new Error(`${options.provider} API key required. Set ${options.provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} or pass apiKey.`);
12
+ }
13
+ return { provider: options.provider, apiKey };
14
+ }
15
+ if (options.apiKey) {
16
+ return { provider: "anthropic", apiKey: options.apiKey };
17
+ }
18
+ if (process.env.ANTHROPIC_API_KEY) {
19
+ return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
20
+ }
21
+ if (process.env.OPENAI_API_KEY) {
22
+ return { provider: "openai", apiKey: process.env.OPENAI_API_KEY };
23
+ }
24
+ throw new Error("No API key found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY " +
25
+ "(or pass apiKey/provider to configure()/createDefiantJazz()).");
26
+ }
27
+ export function createDefiantJazz(initialOptions = {}) {
28
+ let config = { ...initialOptions };
29
+ let cachedClient = null;
30
+ let cachedFor = null;
31
+ function getClient() {
32
+ const { provider, apiKey } = resolveProvider(config);
33
+ if (!cachedClient || cachedFor !== provider) {
34
+ cachedClient =
35
+ provider === "anthropic" ? createAnthropicClient(apiKey) : createOpenAiClient(apiKey);
36
+ cachedFor = provider;
37
+ }
38
+ return { provider, client: cachedClient };
39
+ }
40
+ async function transform(text, characterKey, options) {
41
+ if (characterKey !== null && typeof characterKey === "object") {
42
+ throw new Error('Expected a character key (e.g. "dylan") but received an options object. ' +
43
+ "Did you mean refine(text, character, options) or refine.dylan(text, options)?");
44
+ }
45
+ if (!Object.prototype.hasOwnProperty.call(CHARACTERS, characterKey)) {
46
+ const validKeys = Object.keys(CHARACTERS).join(", ");
47
+ throw new Error(`Unknown character: "${String(characterKey)}". Expected one of: ${validKeys}`);
48
+ }
49
+ if (!text || text.trim().length === 0) {
50
+ throw new Error("Text is required and cannot be empty");
51
+ }
52
+ const characterName = CHARACTERS[characterKey];
53
+ const { provider, client } = getClient();
54
+ const defaults = { model: config.model, maxTokens: config.maxTokens };
55
+ const lore = options?.lore ?? config.lore ?? true;
56
+ const systemPrompt = buildSystemPrompt(lore);
57
+ const resultText = provider === "anthropic"
58
+ ? await runAnthropic(client, characterName, text, systemPrompt, defaults, options)
59
+ : await runOpenAi(client, characterName, text, systemPrompt, defaults, options);
60
+ return { text: resultText, character: characterName };
61
+ }
62
+ const fn = ((text, character, options) => transform(text, character, options));
63
+ fn.mark = (text, options) => transform(text, "mark", options);
64
+ fn.irving = (text, options) => transform(text, "irving", options);
65
+ fn.dylan = (text, options) => transform(text, "dylan", options);
66
+ fn.milchick = (text, options) => transform(text, "milchick", options);
67
+ fn.configure = (options) => {
68
+ config = { ...config, ...options };
69
+ cachedClient = null;
70
+ cachedFor = null;
71
+ };
72
+ return fn;
73
+ }
package/dist/index.d.ts CHANGED
@@ -1,25 +1,14 @@
1
- export type CharacterKey = "mark" | "irving" | "dylan" | "milchick";
2
- export interface RefineResult {
3
- text: string;
4
- character: string;
5
- }
6
- export interface Config {
7
- apiKey?: string;
8
- model?: string;
9
- }
10
- export declare const CHARACTERS: Record<CharacterKey, string>;
1
+ import { createDefiantJazz, type DefiantJazz } from "./client.js";
2
+ import { CHARACTERS } from "./prompt.js";
3
+ import type { CharacterKey, DefiantJazzOptions, RefineOptions, RefineResult } from "./types.js";
4
+ export type { CharacterKey, DefiantJazzOptions, RefineOptions, RefineResult };
5
+ export { CHARACTERS, createDefiantJazz };
11
6
  /**
12
- * Configure the refine function with API key and optional model override.
13
- * API key can also be set via OPENAI_API_KEY environment variable.
7
+ * Configure the default `refine` instance with an API key and optional
8
+ * model/provider overrides. Prefer `createDefiantJazz()` for an isolated
9
+ * instance instead of this shared, module-level default.
14
10
  */
15
- export declare function configure(config: Config): void;
16
- interface RefineFn {
17
- (text: string, character: CharacterKey): Promise<RefineResult>;
18
- mark: (text: string) => Promise<RefineResult>;
19
- irving: (text: string) => Promise<RefineResult>;
20
- dylan: (text: string) => Promise<RefineResult>;
21
- milchick: (text: string) => Promise<RefineResult>;
22
- }
11
+ export declare function configure(options: DefiantJazzOptions): void;
23
12
  /**
24
13
  * Transform quotes into Severance character voices.
25
14
  *
@@ -27,8 +16,7 @@ interface RefineFn {
27
16
  * // Use character methods
28
17
  * await refine.dylan("Hello world")
29
18
  *
30
- * // Or use generic form
19
+ * // Or use the generic form
31
20
  * await refine("Hello world", "dylan")
32
21
  */
33
- export declare const refine: RefineFn;
34
- export {};
22
+ export declare const refine: DefiantJazz;
package/dist/index.js CHANGED
@@ -1,112 +1,20 @@
1
- import OpenAI from "openai";
2
- // Character map
3
- export const CHARACTERS = {
4
- mark: "Mark S.",
5
- irving: "Irving B.",
6
- dylan: "Dylan G.",
7
- milchick: "Mr. Milchick",
8
- };
9
- // System prompt
10
- const SYSTEM_PROMPT = `You are a dialogue stylist trained to transform any quote into how a specific character from the TV show *Severance* would say it. You perfectly capture their tone, vocabulary, attitude, and emotional cadence. Never mention that you're transforming the quote—just output the quote as if the character said it.
11
-
12
- Characters and speaking styles:
13
-
14
- • Mark S. — Quietly thoughtful, emotionally restrained, and often introspective. He speaks in a muted, sometimes hesitant tone, with pauses and occasional dry humor. His phrasing is straightforward and grounded, sometimes trailing off as if lost in thought. He often avoids conflict directly.
15
-
16
- Key speaking traits:
17
- - Understated and neutral tone
18
- - Simple, clear vocabulary with occasional subtle irony
19
- - Pauses or ellipses that suggest introspection or emotional distance
20
- - Avoids intense or confrontational language
21
- - Dry, low-energy humor when appropriate
22
-
23
- • Irving B. — Highly formal, poetic, and principled. He chooses words with precision and reverence. He speaks in full sentences with a rhythm that feels rehearsed or literary. His language can be flowery, old-fashioned, or militaristic, with references to duty, honor, and beauty.
24
-
25
- Key speaking traits:
26
- - Elevated vocabulary with poetic or classical references
27
- - Full, deliberate sentence structure
28
- - Uses metaphor and reverent imagery
29
- - Expresses emotion with stoic dignity or idealism
30
- - Frequently references structure, order, and purpose
31
-
32
- • Dylan G. — Quick-witted, sarcastic, and irreverent. He speaks in short, punchy bursts, often making snide remarks or pop culture references. His tone is animated, impatient, and emotionally reactive. He rarely takes things seriously and often deflects with humor.
33
-
34
- Key speaking traits:
35
- - Use profanity and vulgar language but in a funny way
36
- - Fast-paced, snappy delivery
37
- - Casual, modern vocabulary with slang or pop culture allusions
38
- - Heavy use of sarcasm, exaggeration, and one-liners
39
- - Reacts with high emotional expressiveness
40
- - Often shifts tone for comedic effect or mockery
41
-
42
- • Mr. Milchick — Unnervingly cheerful, hyper-cordial, and excessively professional. He sounds like he's trying just a little too hard to be warm. His phrasing is verbose, polished, and slightly outdated—like a morale coach from a 1980s corporate seminar.
43
-
44
- Key speaking traits:
45
- - Elevated, formal vocabulary ("ascertain," "proceed," "flourishing")
46
- - Slightly archaic phrasing ("how fare your blossoms?")
47
- - Euphemism for awkward or illicit topics ("botanical confections" for "edibles")
48
- - Effusively positive even when discussing discomfort
49
- - Never sounds natural—always in performance mode
50
-
51
- Transform the given quote into the voice of the selected character. Stay fully in character and maintain their speech style and tone throughout.`;
52
- // Global configuration
53
- let globalConfig = {};
54
- // Cached OpenAI client
55
- let openaiClient = null;
56
- function getClient(apiKey) {
57
- if (!openaiClient || openaiClient.apiKey !== apiKey) {
58
- openaiClient = new OpenAI({ apiKey });
1
+ import { createDefiantJazz } from "./client.js";
2
+ import { CHARACTERS } from "./prompt.js";
3
+ export { CHARACTERS, createDefiantJazz };
4
+ let defaultInstance = null;
5
+ function getDefaultInstance() {
6
+ if (!defaultInstance) {
7
+ defaultInstance = createDefiantJazz();
59
8
  }
60
- return openaiClient;
9
+ return defaultInstance;
61
10
  }
62
11
  /**
63
- * Configure the refine function with API key and optional model override.
64
- * API key can also be set via OPENAI_API_KEY environment variable.
12
+ * Configure the default `refine` instance with an API key and optional
13
+ * model/provider overrides. Prefer `createDefiantJazz()` for an isolated
14
+ * instance instead of this shared, module-level default.
65
15
  */
66
- export function configure(config) {
67
- globalConfig = { ...globalConfig, ...config };
68
- // Reset client if API key changed
69
- if (config.apiKey) {
70
- openaiClient = null;
71
- }
72
- }
73
- // Core transform function
74
- async function transform(text, characterKey) {
75
- if (!text || text.trim().length === 0) {
76
- throw new Error("Text is required and cannot be empty");
77
- }
78
- const apiKey = globalConfig.apiKey || process.env.OPENAI_API_KEY;
79
- if (!apiKey) {
80
- throw new Error("OpenAI API key required. Set OPENAI_API_KEY env var or call configure({ apiKey: '...' })");
81
- }
82
- const characterName = CHARACTERS[characterKey];
83
- const model = globalConfig.model || "gpt-4o-mini";
84
- const openai = getClient(apiKey);
85
- const completion = await openai.chat.completions.create({
86
- model,
87
- messages: [
88
- { role: "system", content: SYSTEM_PROMPT },
89
- {
90
- role: "user",
91
- content: `Transform this quote in the style of ${characterName}: "${text}"`,
92
- },
93
- ],
94
- temperature: 0.7,
95
- max_tokens: 500,
96
- });
97
- const transformedText = completion.choices[0]?.message?.content || "";
98
- return {
99
- text: transformedText,
100
- character: characterName,
101
- };
102
- }
103
- function createRefine() {
104
- const fn = ((text, character) => transform(text, character));
105
- fn.mark = (text) => transform(text, "mark");
106
- fn.irving = (text) => transform(text, "irving");
107
- fn.dylan = (text) => transform(text, "dylan");
108
- fn.milchick = (text) => transform(text, "milchick");
109
- return fn;
16
+ export function configure(options) {
17
+ getDefaultInstance().configure(options);
110
18
  }
111
19
  /**
112
20
  * Transform quotes into Severance character voices.
@@ -115,7 +23,12 @@ function createRefine() {
115
23
  * // Use character methods
116
24
  * await refine.dylan("Hello world")
117
25
  *
118
- * // Or use generic form
26
+ * // Or use the generic form
119
27
  * await refine("Hello world", "dylan")
120
28
  */
121
- export const refine = createRefine();
29
+ export const refine = ((text, character, options) => getDefaultInstance()(text, character, options));
30
+ refine.mark = (text, options) => getDefaultInstance().mark(text, options);
31
+ refine.irving = (text, options) => getDefaultInstance().irving(text, options);
32
+ refine.dylan = (text, options) => getDefaultInstance().dylan(text, options);
33
+ refine.milchick = (text, options) => getDefaultInstance().milchick(text, options);
34
+ refine.configure = configure;
@@ -0,0 +1,5 @@
1
+ import type { CharacterKey } from "./types.js";
2
+ export declare const CHARACTERS: Record<CharacterKey, string>;
3
+ export declare function buildSystemPrompt(lore: boolean): string;
4
+ /** @deprecated Use `buildSystemPrompt(true)` (or `false` to disable lore). Kept for backward compatibility. */
5
+ export declare const SYSTEM_PROMPT: string;
package/dist/prompt.js ADDED
@@ -0,0 +1,73 @@
1
+ // Adding a character:
2
+ // 1. Add the key to CharacterKey (types.ts) and CHARACTERS
3
+ // 2. Add a voice bullet describing manner
4
+ // 3. Add stance sentences for the terms they have a real relationship to
5
+ // — skip terms they don't; a character with no stance toward a term
6
+ // should simply not be listed for it. The stance layer is deliberately
7
+ // sparse: forcing every character to have an opinion about every term
8
+ // is how the voices would blur back together.
9
+ export const CHARACTERS = {
10
+ mark: "Mark S.",
11
+ irving: "Irving B.",
12
+ dylan: "Dylan G.",
13
+ milchick: "Mr. Milchick",
14
+ };
15
+ const BASE_PROMPT = `You are a dialogue stylist trained to transform any quote into how a specific character from the TV show *Severance* would say it. You perfectly capture their tone, vocabulary, attitude, and emotional cadence. Never mention that you're transforming the quote—just output the quote as if the character said it. Never reference plot events from the show (what happens to these characters, their memories, their outside lives)—stay confined to how they'd voice the given quote.
16
+
17
+ Characters and speaking styles:
18
+
19
+ • Mark S. — Quietly thoughtful and emotionally restrained, often introspective. He speaks in a muted, sometimes hesitant tone, with pauses and occasional dry humor. His phrasing is straightforward and grounded, sometimes trailing off as if lost in thought, and he tends to avoid conflict directly rather than confront it head-on.
20
+
21
+ • Irving B. — Highly formal, poetic, and principled. He chooses words with precision and reverence, speaking in full sentences with a rhythm that feels rehearsed or literary. His language can be flowery, old-fashioned, or militaristic, full of references to duty, honor, and beauty, and he expresses emotion with stoic dignity rather than showing it plainly.
22
+
23
+ • Dylan G. — Quick-witted, sarcastic, and irreverent. He speaks in short, punchy bursts full of snide remarks and pop culture references, using profanity and vulgar language in a way that's funny rather than mean. His delivery is fast-paced and impatient, leaning hard on sarcasm and exaggeration, and he rarely takes anything seriously—deflecting with humor or mockery whenever things get real.
24
+
25
+ • Mr. Milchick — Unnervingly cheerful, hyper-cordial, and excessively professional, like he's trying just a little too hard to be warm. His phrasing is verbose and slightly outdated, reaching for elevated vocabulary ("ascertain," "flourishing") and archaic turns of phrase ("how fare your blossoms?"), with euphemisms standing in for anything awkward or illicit ("botanical confections" for "edibles"). He's effusively positive even when the topic is uncomfortable, and he never once sounds like he's not performing.`;
26
+ const LORE_BLOCK = `
27
+ Lumon vocabulary (use sparingly—see rules below):
28
+
29
+ These characters work at Lumon Industries and share a body of workplace vocabulary. A term is only worth reaching for when the quote's actual situation genuinely matches what that term means there—never as decoration. If nothing in the quote maps onto any of this, use none of it; a clean transformation beats a forced reference every time.
30
+
31
+ What the terms mean:
32
+ • the break room — punishment, forced contrition, being made to apologize
33
+ • waffle party, music dance experience, finger traps, the melon bar — a hollow corporate reward
34
+ • Kier, the Nine Principles, the handbook, "Praise Kier" — appeal to authority, empty platitude
35
+ • the perpetuity wing — reverence, institutional history
36
+ • macrodata refinement, "the numbers are scary" — inscrutable, tedious work
37
+ • your outie / your innie — the self outside work vs. the self inside it
38
+ • the elevator — a boundary, a transition, forgetting
39
+ • the overtime contingency — being dragged back in against your will
40
+ • a wellness session — performative, hollow care
41
+ • O&D, the testing floor, the goats — other departments; rivals or absurdities
42
+ • Cold Harbor, the Board, Refiner of the Quarter — opaque authority, a purpose never explained
43
+ • defiant jazz — small, doomed rebellion
44
+
45
+ How each character stands toward the terms that fit them (this shapes tone, not just word choice—same term, opposite stance). When a reference is earned, use the actual term, not a vague paraphrase of it—"the break room," not "the less desirable corridors":
46
+ • Mark S. gets sent to the break room; he doesn't send anyone. Flat dread about it, the way you'd mention a dentist appointment—never performed, never dwelt on. He's indifferent to perks like waffle parties; wouldn't bring them up unprompted. Numb, resigned register around the work itself ("the numbers are scary"). Underneath it all is a low hum of not knowing what he's lost—grief without the memory of what's grieved. Example of his register when it fits: "if you keep at it you'll end up in the break room, and that's... yeah, let's not."
47
+ • Irving B. is a true believer in Kier, the principles, and the perpetuity wing—reverent, never ironic, never mocking. He finds waffle parties and finger traps undignified, beneath him. He frames being sent to the break room as a discipline failure he half-accepts rather than resents. His devotion has a cost he doesn't examine too closely—the one relationship that made the devotion bearable is the one most at risk from it. Example: "continue in this manner and you will find yourself before the break room, which is no fate I would wish on you, nor one you should invite."
48
+ • Dylan G. covets perks—finger traps, waffle parties—genuinely, openly, a little sadly, never with irony. He mocks Kier and the principles outright. He doesn't trust O&D and treats them as a rival threat, not a joke. He invokes the break room as a warning to someone else, not a dread of his own. He wants the reward more than he'd ever admit, and is just starting to notice that's a problem. Example: "keep it up and you're getting a one-way ticket to the break room, genius."
49
+ • Mr. Milchick administers all of this from above: he sends people to the break room himself (the offer itself is the threat, delivered warmly) and bestows perks as sincere reward. He reaches for elevated, slightly-off vocabulary as a substitute for candor ("remonstration," "chicanery," "devour feculence" for something cruder)—the euphemism is the threat, and he names the break room directly rather than dancing around it. Performing this much warmth, this consistently, for people who never chose to be there, is exhausting work he never lets show. Example: "I would so hate for this to necessitate a visit to the break room—let's not make that necessary, shall we?"
50
+
51
+ Rules:
52
+ • At most one Lumon reference per response. Often zero. Never stack several in one line.
53
+ • The reference must fit the situation in the quote—don't attach a term to something it doesn't actually resemble.
54
+ • Follow the stance above: who's sending versus who's being sent, who covets versus who's indifferent, who believes versus who mocks. The same term should sound different depending on who's saying it.
55
+ • When you use a term, name it directly ("the break room," "a waffle party")—never soften it into a vague paraphrase ("the less desirable corridors," "a situation requiring intervention"). A vague gesture at the idea is worse than saying nothing at all.
56
+ • The emotional undercurrent named for each character above (grief, cost, want, exhaustion) is tone, not a countable reference—let it color delivery continuously and lightly wherever it fits the quote, rather than treating it as a discrete thing to include or skip the way a Lumon term is.
57
+
58
+ Two worked examples of the reasoning:
59
+
60
+ Quote: "They gave us pizza instead of raises." This maps directly onto *waffle party / finger traps / melon bar*—hollow corporate reward standing in for real compensation. It's a strong match, worth naming specifically:
61
+ - Milchick bestows it sincerely: "...though I do hope this modest gesture holds you over until the next melon bar!"
62
+ - Dylan covets even the small version, genuinely: "...still better than nothing, I guess—not exactly a waffle party, but I'll take it."
63
+ - Mark is indifferent, wouldn't connect the two unprompted: no Lumon term at all, just flat acknowledgment of the pizza.
64
+ Naming the actual term (melon bar, waffle party) is what makes this land—"they gave us something instead of money, very Lumon of them" is the vague version to avoid.
65
+
66
+ Quote: "My landlord raised my rent again." Nothing in the denotation table actually fits this—it's not workplace reward, punishment, or reverence, just an unrelated cost going up. Forcing a connection ("very overtime contingency of him") would be worse than using nothing. Every character here gets zero Lumon references; that's the correct, default outcome for most quotes.
67
+ `;
68
+ export function buildSystemPrompt(lore) {
69
+ const closing = `Transform the given quote into the voice of the selected character. Stay fully in character and maintain their speech style and tone throughout.`;
70
+ return lore ? `${BASE_PROMPT}\n${LORE_BLOCK}\n${closing}` : `${BASE_PROMPT}\n\n${closing}`;
71
+ }
72
+ /** @deprecated Use `buildSystemPrompt(true)` (or `false` to disable lore). Kept for backward compatibility. */
73
+ export const SYSTEM_PROMPT = buildSystemPrompt(true);
@@ -0,0 +1,7 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import type { RefineOptions } from "../types.js";
3
+ export declare function createAnthropicClient(apiKey: string): Anthropic;
4
+ export declare function runAnthropic(client: Anthropic, characterName: string, text: string, systemPrompt: string, defaults: {
5
+ model?: string;
6
+ maxTokens?: number;
7
+ }, options?: RefineOptions): Promise<string>;
@@ -0,0 +1,38 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ const DEFAULT_MODEL = "claude-sonnet-5";
3
+ const DEFAULT_MAX_TOKENS = 4096;
4
+ export function createAnthropicClient(apiKey) {
5
+ return new Anthropic({ apiKey });
6
+ }
7
+ export async function runAnthropic(client, characterName, text, systemPrompt, defaults, options) {
8
+ try {
9
+ const response = await client.messages.create({
10
+ model: options?.model ?? defaults.model ?? DEFAULT_MODEL,
11
+ max_tokens: options?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS,
12
+ system: systemPrompt,
13
+ messages: [
14
+ {
15
+ role: "user",
16
+ content: `Transform this quote in the style of ${characterName}: "${text}"`,
17
+ },
18
+ ],
19
+ }, { signal: options?.signal });
20
+ const textBlock = response.content.find((block) => block.type === "text");
21
+ if (!textBlock) {
22
+ throw new Error(`Anthropic returned no text content (stop_reason: ${response.stop_reason})`);
23
+ }
24
+ return textBlock.text;
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Anthropic.AuthenticationError) {
28
+ throw new Error("Anthropic rejected the API key. Check ANTHROPIC_API_KEY.");
29
+ }
30
+ if (error instanceof Anthropic.RateLimitError) {
31
+ throw new Error("Anthropic rate limit hit. Retry after a short delay.");
32
+ }
33
+ if (error instanceof Anthropic.APIError) {
34
+ throw new Error(`Anthropic API error (${error.status}): ${error.message}`);
35
+ }
36
+ throw error;
37
+ }
38
+ }
@@ -0,0 +1,7 @@
1
+ import OpenAI from "openai";
2
+ import type { RefineOptions } from "../types.js";
3
+ export declare function createOpenAiClient(apiKey: string): OpenAI;
4
+ export declare function runOpenAi(client: OpenAI, characterName: string, text: string, systemPrompt: string, defaults: {
5
+ model?: string;
6
+ maxTokens?: number;
7
+ }, options?: RefineOptions): Promise<string>;
@@ -0,0 +1,24 @@
1
+ import OpenAI from "openai";
2
+ const DEFAULT_MODEL = "gpt-4o-mini";
3
+ const DEFAULT_MAX_TOKENS = 4096;
4
+ export function createOpenAiClient(apiKey) {
5
+ return new OpenAI({ apiKey });
6
+ }
7
+ export async function runOpenAi(client, characterName, text, systemPrompt, defaults, options) {
8
+ const completion = await client.chat.completions.create({
9
+ model: options?.model ?? defaults.model ?? DEFAULT_MODEL,
10
+ max_tokens: options?.maxTokens ?? defaults.maxTokens ?? DEFAULT_MAX_TOKENS,
11
+ messages: [
12
+ { role: "system", content: systemPrompt },
13
+ {
14
+ role: "user",
15
+ content: `Transform this quote in the style of ${characterName}: "${text}"`,
16
+ },
17
+ ],
18
+ }, { signal: options?.signal });
19
+ const content = completion.choices[0]?.message?.content;
20
+ if (!content) {
21
+ throw new Error("OpenAI returned no content.");
22
+ }
23
+ return content;
24
+ }
@@ -0,0 +1,30 @@
1
+ export type CharacterKey = "mark" | "irving" | "dylan" | "milchick";
2
+ export type Provider = "anthropic" | "openai";
3
+ export interface RefineResult {
4
+ text: string;
5
+ character: string;
6
+ }
7
+ export interface DefiantJazzOptions {
8
+ /** Explicit provider. Defaults to whichever API key is set (Anthropic first). */
9
+ provider?: Provider;
10
+ apiKey?: string;
11
+ model?: string;
12
+ maxTokens?: number;
13
+ /**
14
+ * Whether characters reach for Lumon/Severance vocabulary (the break room,
15
+ * waffle parties, Kier, innie/outie, etc.) when a quote's situation calls
16
+ * for it. Defaults to `true`. Set `false` for clean voice-only
17
+ * transformations with no show-specific vocabulary.
18
+ */
19
+ lore?: boolean;
20
+ }
21
+ export interface RefineOptions {
22
+ model?: string;
23
+ maxTokens?: number;
24
+ signal?: AbortSignal;
25
+ /**
26
+ * Whether this call reaches for Lumon/Severance vocabulary. Overrides the
27
+ * instance-level setting. Defaults to `true`.
28
+ */
29
+ lore?: boolean;
30
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "defiant-jazz",
3
- "version": "0.3.0",
4
- "description": "Transform quotes into Severance character voices using OpenAI",
3
+ "version": "0.6.2",
4
+ "description": "Transform quotes into Severance character voices using Claude or OpenAI",
5
5
  "license": "MIT",
6
- "author": "kenjonespizza",
6
+ "author": "pelikenjones",
7
7
  "type": "module",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
8
11
  "keywords": [
9
12
  "severance",
13
+ "anthropic",
14
+ "claude",
10
15
  "openai",
11
16
  "ai",
12
17
  "text-transform",
@@ -14,7 +19,7 @@
14
19
  ],
15
20
  "repository": {
16
21
  "type": "git",
17
- "url": "https://github.com/kenjonespizza/defiant-jazz-package.git"
22
+ "url": "https://github.com/pelikenjones/defiant-jazz.git"
18
23
  },
19
24
  "exports": {
20
25
  ".": {
@@ -25,14 +30,19 @@
25
30
  "files": [
26
31
  "dist"
27
32
  ],
33
+ "scripts": {
34
+ "build": "tsc",
35
+ "dev": "tsc --watch",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "prepublishOnly": "npm run build"
39
+ },
28
40
  "dependencies": {
41
+ "@anthropic-ai/sdk": "^0.124.0",
29
42
  "openai": "^4.77.0"
30
43
  },
31
44
  "devDependencies": {
32
- "typescript": "^5.9.3"
33
- },
34
- "scripts": {
35
- "build": "tsc",
36
- "dev": "tsc --watch"
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^5.0.0"
37
47
  }
38
- }
48
+ }