// Circle Suite — Voice Profile (level 4). // // The Copy Engine writes in the TEAM voice. That is the right default: it is // compliant, it is consistent, and a brand-new member has no voice of their own // to write in yet. But a member who has been at this a while sounds like // themselves, and copy that sounds like the team instead of like them is the // first thing their own audience notices. // // So this is not a second engine. It is a small profile that rides along with // every Copy Engine and Email Engine generation and biases the output toward // how this particular person actually talks. // // Deliberately kept short. Five specific answers beat a long questionnaire: the // profile becomes prompt text, and prompt text that rambles just dilutes the // instructions that matter (the compliance block especially). 'use strict'; const fs = require('fs'); const path = require('path'); let DATA_DIR = null; function init(opts) { DATA_DIR = opts.dataDir; try { fs.mkdirSync(dir(), { recursive: true }); } catch (e) {} } function dir() { return path.join(DATA_DIR, 'voice'); } function file(id) { return path.join(dir(), String(Number(id)) + '.json'); } // field -> {label, hint, max}. Order is the order they appear in the form. const FIELDS = [ { key: 'background', label: 'What did you do before this?', hint: 'A trade, a job, a business, retired, still working — one line is plenty.', max: 220 }, { key: 'audience', label: 'Who are you usually talking to?', hint: 'Friends and family? Other marketers? People in your church, your gym, your old industry?', max: 220 }, { key: 'tone', label: 'How would a friend describe the way you talk?', hint: 'Blunt. Funny. Careful. Encouraging. Skeptical. Pick a few real words.', max: 160 }, { key: 'never', label: 'What would you never say?', hint: 'Words or promises that would make you cringe coming out of your own mouth.', max: 220 }, { key: 'sample', label: 'Paste something you actually wrote', hint: 'A post, a text, an email — anything a few sentences long, in your own words. This teaches it more than the rest combined.', max: 900 } ]; function blank() { const o = {}; FIELDS.forEach(function (f) { o[f.key] = ''; }); return o; } function load(id) { try { return JSON.parse(fs.readFileSync(file(id), 'utf8')); } catch (e) { return null; } } function save(id, body) { const rec = blank(); FIELDS.forEach(function (f) { rec[f.key] = String((body && body[f.key]) || '').replace(/\s+/g, ' ').trim().slice(0, f.max); }); rec.id = Number(id); rec.updatedAt = new Date().toISOString(); rec.complete = complete(rec); try { fs.writeFileSync(file(id), JSON.stringify(rec)); } catch (e) {} return rec; } function clear(id) { try { fs.unlinkSync(file(id)); } catch (e) {} return true; } // "Complete enough to be worth using" — not every field, because the writing // sample alone carries most of the signal. function complete(rec) { if (!rec) return false; const filled = FIELDS.filter(function (f) { return String(rec[f.key] || '').trim().length > 2; }).length; return filled >= 2 && String(rec.sample || rec.tone || '').trim().length > 10; } // Render the profile as prompt text. Returns '' when there is nothing useful, // so callers can concatenate unconditionally. function promptBlock(id) { const rec = load(id); if (!complete(rec)) return ''; const bits = []; if (rec.background) bits.push('Their background: ' + rec.background); if (rec.audience) bits.push('Who they are writing to: ' + rec.audience); if (rec.tone) bits.push('How they come across: ' + rec.tone); if (rec.never) bits.push('Things they would never say — avoid these completely: ' + rec.never); if (rec.sample) bits.push('A sample of their own writing, for rhythm and word choice (do NOT copy its content, only its voice):\n"""\n' + rec.sample + '\n"""'); if (!bits.length) return ''; return 'WRITE AS THIS PERSON. Match their rhythm, vocabulary and level of formality. ' + 'This overrides the generic team voice above, EXCEPT for the compliance rules, which are absolute and can never be relaxed to sound more like them.\n' + bits.join('\n'); } module.exports = { init, FIELDS, blank, load, save, clear, complete, promptBlock };