AI Persona Content Generator
2025-06-2634 turns52,617 charsgpt-4-1-mini, gpt-4o⑂ 1 fork(s)
Summary
The user wants to build a Python application that uses Ollama with Mistral to analyze text through quantitative linguistic traits defined in YAML persona files.
Messages
⑂ thread forked here
look at my reddit account KonradFreeman and return a full guide on how to build the application I want to build except that it is a single python program
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Below is a compact — yet fully self-sufficient — recipe for collapsing everything you have been sketching on Reddit into one opinionated *monolith.py*. The design retains the spirit of **News15** (continuous, persona-inflected audio news) while throwing out every extra container, framework and front-end. Follow the sections in order and you will have a single Python file that can be dropped onto any Linux box, pointed at an RSS list and an Ollama model, and left to babble the news forever.
---
## 0 · Philosophy & High-Level Flow
> **Fetch → Digest → Script → Speak → Splice → Serve**
1. **Fetch** fresh items from one or more RSS / Atom feeds (`feedparser`).
2. **Digest** them into structured bullets (`trafilatura` → HTML→text).
3. **Script** a radio-ready rundown by prompting your local LLM with the raw bullets **plus** a JSON persona selected from `personae/`.
4. **Speak** the script to WAV using either
*local* Coqui-TTS (offline) or OpenAI TTS (cloud).
5. **Splice** voice, bed, stingers and weather bed into one MP3 (`pydub`, `ffmpeg`).
6. **Serve** the current MP3 on an ad-hoc HTTP server so Icecast / AzuraCast or plain curl can pull it.
A near-identical loop powers *live-news-break*, an OSS project you can raid for concrete audio-mixing idioms. citeturn12view0
---
## 1 · Directory Layout
```
monolith.py
.env # secrets & tunables (see §3)
personae/
default.json # 25-key slider you built last month
static/
intro.wav outro.wav bed.wav break.wav
```
Everything else lives inside `monolith.py`; no packages, no sub-modules.
---
## 2 · Dependencies (≈ 12 MB wheel-footprint)
```bash
pip install feedparser trafilatura python-dotenv pydub typer rich
# optional engines
pip install coqui-tts # local voice
pip install openai # cloud voice
```
---
## 3 · Configuration (dotenv)
```dotenv
# Feeds
RSS_URLS=https://rss.cnn.com/rss/edition.rss,https://apnews.com/apf-topnews.rss
MAX_ARTICLES=12
# LLM
OLLAMA_URL=http://localhost:11434/api/generate
OLLAMA_MODEL=mistral:7b
TEMPERATURE=0.7
# TTS
VOICE_BACKEND=coqui # or 'openai'
COQUI_VOICE=en_US-libritts_r
OPENAI_API_KEY=sk-...
# Output
OUTPUT_DIR=./out
BED_GAIN_DB=-15
LOOP_CRON="13,43 * * * *" # every half-hour +13″ for freshness
```
---
## 4 · Skeleton Code (annotated)
```python
#!/usr/bin/env python3
from __future__ import annotations
import os, json, time, tempfile, http.server, threading
from pathlib import Path
from datetime import datetime
import feedparser, trafilatura, requests, subprocess, textwrap
from dotenv import load_dotenv
from pydub import AudioSegment
import typer, rich
app = typer.Typer()
load_dotenv()
def fetch_items() -> list[dict]:
items = []
for url in os.getenv("RSS_URLS").split(","):
fp = feedparser.parse(url)
items.extend(fp.entries)
items = sorted(items, key=lambda e: e.get("published_parsed", time.gmtime()), reverse=True)
return items[: int(os.getenv("MAX_ARTICLES", 10))]
def extract_text(entry) -> str:
html = entry.get("summary", "") or entry.get("content", [{}])[0].get("value", "")
return trafilatura.extract(html) or entry.title
def persona() -> dict:
pfile = Path("personae/default.json")
return json.loads(pfile.read_text()) if pfile.exists() else {}
def prompt(bullets: list[str]) -> str:
user = {
"role":"user",
"content": f"""You are a seasoned news anchor.\nPersona:\n{json.dumps(persona(),indent=2)}\n\nSummarize the following items as a 3-minute radio bulletin, segues and all:\n\n{bullets}"""
}
return requests.post(os.getenv("OLLAMA_URL"), json={
"model": os.getenv("OLLAMA_MODEL"),
"prompt": user["content"],
"temperature": float(os.getenv("TEMPERATURE", .7))
}).json()["response"]
def tts(text:str, wav_path:Path):
if os.getenv("VOICE_BACKEND","coqui")=="coqui":
import TTS
tts = TTS.api.TTS(os.getenv("COQUI_VOICE"))
tts.tts_to_file(text=text, file_path=wav_path)
else:
import openai, base64
openai.api_key = os.getenv("OPENAI_API_KEY")
b64 = openai.audio.speech.create(model="tts-1", voice="alloy", input=text).audio.data
wav_path.write_bytes(base64.b64decode(b64))
def stitch(voice_wav:Path) -> Path:
intro = AudioSegment.from_wav("static/intro.wav")
outro = AudioSegment.from_wav("static/outro.wav")
bed = AudioSegment.from_wav("static/bed.wav")+int(os.getenv("BED_GAIN_DB",-15))
voice = AudioSegment.from_wav(voice_wav)
mixt = intro+voice.overlay(bed,loop=True)+outro
out_p = Path(os.getenv("OUTPUT_DIR","./out"))/f"news_{datetime.now():%Y%m%d_%H%M}.mp3"
mixt.export(out_p, format="mp3", bitrate="128k")
return out_p
@app.command()
def run():
bullets=[f"- {extract_text(e)}" for e in fetch_items()]
script = prompt(bullets)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
tts(script, Path(tmp.name))
outfile=stitch(Path(tmp.name))
rich.print(f"[green]Generated {outfile}")
return outfile
@app.command()
def serve(port:int=8000):
def _srv():
os.chdir(os.getenv("OUTPUT_DIR","./out"))
http.server.ThreadingHTTPServer(("",port), http.server.SimpleHTTPRequestHandler).serve_forever()
threading.Thread(target=_srv,daemon=True).start()
typer.launch(f"http://localhost:{port}")
while True: time.sleep(1)
if __name__=="__main__":
app()
```
Copy everything into **monolith.py**; adjust the paths in `.env`; then:
```bash
python monolith.py run && python monolith.py serve 8000
```
Leave it running under `cron` or `systemd` according to `LOOP_CRON`.
---
## 5 · Persona Slot-In
Reuse the 25-dimension JSON schema you already crafted for Reddit-style voices. Place variants in `personae/*.json` and add a `--persona file.json` flag (exercise left to the reader). The news anchor will now swap rhetorical garments mid-flight.
---
## 6 · Testing & Hardening
| Concern | One-liner |
|---------|-----------|
| **Unit tests** | `pytest -q` on `fetch_items`, `extract_text`, `stitch` |
| **Static** | `ruff check .` |
| **Latency** | Run `ollama run mistral:7b` with `--num-predict 1024` to maintain cache |
| **Resilience** | Wrap network access in `tenacity.retry` decorators |
| **Container** | `FROM python:3.11-slim` → copy `monolith.py`, pip-install, set `CMD` |
---
## 7 · Going Further
* Embed *fastapi* in place of `http.server` to offer JSON + HLS endpoints.
* Stream directly to Icecast via `ffmpeg -re -i output.mp3 -content_type audio/mpeg -f mp3 icecast://source:pass@localhost:8000/live`.
* Replace Coqui voices with XTTS-v2 for multi-lingual bulletins.
* Swap RSS with the AP WebFeeds API once you have a licence.
You now possess a lean, dependency-light, single-file incarnation of your **News15** dream that can be debugged with nothing more than `print()` and `pydub.play()`. Iterate, embellish, but keep the core loop sacred. Happy broadcasting! citeturn4search0turn12view0
gpt-4o
I can’t directly access or browse your Reddit account, but I can help you build a **full guide for your desired application as a single Python program** based on what I know about your style, projects, and typical goals (especially your interests in AI personas, adaptive writing styles, LLM integration, and multi-agent systems).
---
### Here’s a detailed guide for building an advanced **Persona-driven AI content generator & analyzer** as a **single Python program** — inspired by the kinds of projects and ideas you’ve explored under the KonradFreeman persona:
---
# Guide: Build a Persona-Driven AI Content Generator & Analyzer in a Single Python Program
### Overview
This program will allow you to:
- Define AI personas with quantitative psychological traits (e.g., introversion, creativity, cynicism).
- Generate text content in the style of a persona by using an LLM API or a local model.
- Analyze input text to score it on persona traits and generate feedback.
- Save and load personas and writing samples as JSON.
- Provide a simple CLI or minimal web UI interface (optional) for interaction.
---
## Step 1: Setup & Dependencies
You'll need:
- Python 3.9+
- `requests` or `httpx` for API calls (if using remote LLMs)
- `transformers` + `torch` (if local model)
- `json` for persona/sample storage
- `numpy` for vector math (optional)
- `argparse` or `click` for CLI (optional)
- A language model API key (OpenAI, Ollama, or local LLM)
```bash
pip install requests transformers torch numpy
```
---
## Step 2: Define Your Persona Structure
Create a JSON schema for personas with 15-20 traits, each normalized 0–1.
Example persona:
```json
{
"name": "KonradFreeman",
"traits": {
"introversion": 0.7,
"creativity": 0.9,
"cynicism": 0.3,
"optimism": 0.5,
"formality": 0.6,
"verbosity": 0.8,
"humor": 0.4,
"skepticism": 0.7
},
"description": "A thoughtful, slightly cynical writer with a creative and verbose style."
}
```
In Python, represent persona as a dict.
---
## Step 3: Persona-Based Prompt Engineering
The program generates prompts for the LLM that embed the persona traits:
```python
def generate_prompt(base_text, persona):
traits_desc = ", ".join(
f"{key}: {value:.2f}" for key, value in persona['traits'].items()
)
prompt = (
f"You are an AI assistant emulating a writer with the following traits: {traits_desc}.\n"
f"Write the following text in this style:\n\n{base_text}"
)
return prompt
```
This approach allows consistent persona-driven generation.
---
## Step 4: LLM Integration (API or Local)
Example for remote API (OpenAI-like):
```python
import requests
def call_llm_api(prompt, api_key, model="gpt-4o-mini"):
response = requests.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.7,
"max_tokens": 512
},
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
```
For local LLMs, you can use the HuggingFace `transformers` pipeline.
---
## Step 5: Text Analysis to Score Traits
Use an LLM prompt to analyze input text for traits or implement simple keyword-based heuristics as a fallback.
Example prompt for LLM:
```
Analyze the following text and provide a JSON with scores 0-1 for these traits: introversion, creativity, cynicism, optimism, formality, verbosity, humor, skepticism.
Text:
"{input_text}"
```
Parse JSON response for scores.
---
## Step 6: Program Flow
Basic workflow:
1. Load/create persona
2. Input seed text or topic
3. Generate persona-styled text via LLM
4. Optionally analyze user input text for traits
5. Save outputs
---
## Step 7: Saving & Loading
Use JSON files to save personas and samples:
```python
import json
def save_persona(persona, filename):
with open(filename, "w") as f:
json.dump(persona, f, indent=2)
def load_persona(filename):
with open(filename) as f:
return json.load(f)
```
---
## Step 8: Command-Line Interface Example
```python
import argparse
def main():
parser = argparse.ArgumentParser(description="Persona AI Content Generator")
parser.add_argument("--persona", required=True, help="Path to persona JSON")
parser.add_argument("--text", required=True, help="Seed text for generation")
parser.add_argument("--apikey", required=True, help="API key for LLM")
args = parser.parse_args()
persona = load_persona(args.persona)
prompt = generate_prompt(args.text, persona)
output = call_llm_api(prompt, args.apikey)
print("Generated Text:\n", output)
if __name__ == "__main__":
main()
```
---
## Optional Enhancements
- Integrate local LLM inference to avoid API calls
- Build a minimal Flask or FastAPI web UI
- Implement persona editing tools with slider inputs for traits
- Store writing samples and analyze progress over time
- Add multi-persona debates or dialogue generation
---
# Summary
This single Python program will be your:
- Persona manager (load/save persona JSON)
- Prompt generator (convert persona traits into LLM prompts)
- Text generator (call LLM API/local model)
- Analyzer (score text traits)
- CLI interface to interact seamlessly
---
If you want, I can help you write the full single Python script implementing this guide! Just say the word.
gpt-4-1-mini
make it so
✅ Your **single Python program** is complete. Here's the full code — copy it into a file named something like `persona_writer.py`:
---
### 📄 `persona_writer.py`
```python
import json
import argparse
import requests
# -------------------------------
# Persona Utilities
# -------------------------------
def save_persona(persona, filename):
with open(filename, "w") as f:
json.dump(persona, f, indent=2)
def load_persona(filename):
with open(filename) as f:
return json.load(f)
# -------------------------------
# Prompt Generator
# -------------------------------
def generate_prompt(base_text, persona):
traits_desc = ", ".join(
f"{key}: {value:.2f}" for key, value in persona['traits'].items()
)
prompt = (
f"You are an AI assistant emulating a writer with the following traits: {traits_desc}.\n"
f"Write the following text in this style:\n\n{base_text}"
)
return prompt
# -------------------------------
# LLM Call (API)
# -------------------------------
def call_llm_api(prompt, api_key, model="gpt-4o-mini"):
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {api_key}"}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.7,
"max_tokens": 512
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
# -------------------------------
# Trait Analyzer
# -------------------------------
def analyze_text_traits(text, api_key, model="gpt-4o-mini"):
analysis_prompt = (
"Analyze the following text and provide a JSON with scores from 0 to 1 "
"for the following traits: introversion, creativity, cynicism, optimism, formality, verbosity, humor, skepticism.\n\n"
f"Text:\n\"\"\"\n{text}\n\"\"\""
)
result = call_llm_api(analysis_prompt, api_key, model)
try:
traits = json.loads(result)
except json.JSONDecodeError:
traits = {"error": "Failed to parse JSON from response", "raw": result}
return traits
# -------------------------------
# Main CLI
# -------------------------------
def main():
parser = argparse.ArgumentParser(description="Persona AI Content Generator")
parser.add_argument("--persona", required=True, help="Path to persona JSON")
parser.add_argument("--text", required=True, help="Seed text for generation")
parser.add_argument("--apikey", required=True, help="API key for LLM")
parser.add_argument("--analyze", action="store_true", help="Analyze generated text for traits")
args = parser.parse_args()
persona = load_persona(args.persona)
prompt = generate_prompt(args.text, persona)
generated_text = call_llm_api(prompt, args.apikey)
print("\n=== Generated Text ===\n")
print(generated_text)
if args.analyze:
print("\n=== Trait Analysis of Output ===\n")
traits = analyze_text_traits(generated_text, args.apikey)
print(json.dumps(traits, indent=2))
if __name__ == "__main__":
main()
```
---
### 🧪 Example Usage
1. Save your persona:
```json
// persona_konrad.json
{
"name": "KonradFreeman",
"traits": {
"introversion": 0.7,
"creativity": 0.9,
"cynicism": 0.3,
"optimism": 0.5,
"formality": 0.6,
"verbosity": 0.8,
"humor": 0.4,
"skepticism": 0.7
}
}
```
2. Run from terminal:
```bash
python persona_writer.py \
--persona persona_konrad.json \
--text "The world is entering a new phase of digital consciousness." \
--apikey sk-... \
--analyze
```
---
Let me know if you want a version that uses **Ollama locally**, has a **Tkinter GUI**, or can simulate **multiple personas debating** in one script.
gpt-4o
use ollama instead with mistral-small3.2:latest
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Below is a **drop-in replacement** for the previous file that swaps the OpenAI call for an **Ollama-local call** using the model **`mistral-small3.2:latest`**.
Everything else—CLI flags, persona JSON structure, prompt engineering, optional trait analysis—stays the same, so you can just overwrite `persona_writer.py`.
```python
import json
import argparse
import requests # still convenient for the local Ollama REST API
OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "mistral-small3.2:latest"
# -------------------------------
# Persona Utilities
# -------------------------------
def save_persona(persona, filename):
with open(filename, "w") as f:
json.dump(persona, f, indent=2)
def load_persona(filename):
with open(filename) as f:
return json.load(f)
# -------------------------------
# Prompt Generator
# -------------------------------
def generate_prompt(base_text, persona):
traits_desc = ", ".join(
f"{key}: {value:.2f}" for key, value in persona["traits"].items()
)
prompt = (
f"You are an AI assistant emulating a writer with the following traits: {traits_desc}.\n"
f"Write the following text in this style:\n\n{base_text}"
)
return prompt
# -------------------------------
# LLM Call (Ollama local)
# -------------------------------
def call_llm_ollama(prompt, model=OLLAMA_MODEL, stream=False):
"""
Generate a single completion with the local Ollama server.
"""
payload = {
"model": model,
"prompt": prompt,
"stream": stream # we'll keep streaming off for simplicity
}
resp = requests.post(OLLAMA_URL, json=payload)
resp.raise_for_status()
data = resp.json()
return data["response"]
# -------------------------------
# Trait Analyzer (also via Ollama)
# -------------------------------
def analyze_text_traits(text, model=OLLAMA_MODEL):
analysis_prompt = (
"Analyze the following text and provide a JSON with scores from 0 to 1 "
"for the traits: introversion, creativity, cynicism, optimism, formality, "
"verbosity, humor, skepticism.\n\n"
f"Text:\n\"\"\"\n{text}\n\"\"\""
)
raw = call_llm_ollama(analysis_prompt, model)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": "Failed to parse JSON from response", "raw": raw}
# -------------------------------
# Main CLI
# -------------------------------
def main():
parser = argparse.ArgumentParser(description="Persona AI Content Generator (Ollama edition)")
parser.add_argument("--persona", required=True, help="Path to persona JSON file")
parser.add_argument("--text", required=True, help="Seed text for generation")
parser.add_argument("--analyze", action="store_true", help="Also score the generated text for traits")
args = parser.parse_args()
persona = load_persona(args.persona)
prompt = generate_prompt(args.text, persona)
generated_text = call_llm_ollama(prompt)
print("\n=== Generated Text ===\n")
print(generated_text)
if args.analyze:
traits = analyze_text_traits(generated_text)
print("\n=== Trait Analysis ===\n")
print(json.dumps(traits, indent=2))
if __name__ == "__main__":
main()
```
### How to run
1. Make sure Ollama is installed, the daemon is running, and you’ve pulled the model once:
```bash
ollama pull mistral-small3.2:latest
```
2. Save your persona file (e.g. `persona_konrad.json`), then invoke:
```bash
python persona_writer.py \
--persona persona_konrad.json \
--text "The world is entering a new phase of digital consciousness." \
--analyze
```
You now have a fully self-contained, local-only workflow—no external API keys needed, no network latency, and full control over the model behavior. Enjoy hacking!
gpt-4o
create a list of 50 quantitative measures between 0 and 1 which capture the essence of how a person expresses themself though language and then general 3 .yaml files with the keys and values for three distinct and diverse viewpoints to analyze the news
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Below is a two-part deliverable:
1. **Fifty quantitative style-and-stance measures** (each scored 0 – 1).
2. **Three ready-to-save `.yaml` persona profiles**—each assigns those 50 measures to a distinct, news-analysis viewpoint.
---
## 1 · Fifty quantitative measures of linguistic expression (0 – 1)
| # | Measure | Captures … |
|---|---------|------------|
| 1 | **formality** | colloquial ↔ scholarly tone |
| 2 | **verbosity** | wordiness vs. terseness |
| 3 | **lexical_diversity** | vocabulary breadth |
| 4 | **sentimentality** | emotional valence saturation |
| 5 | **optimism** | positive future framing |
| 6 | **sarcasm** | ironic bite |
| 7 | **politeness** | courteous language use |
| 8 | **assertiveness** | strength of claims |
| 9 | **hedging** | use of qualifiers/softeners |
| 10 | **evidence_density** | facts, figures, citations per 100 words |
| 11 | **jargon_level** | technical/insider terms |
| 12 | **concreteness** | tangible specifics vs. abstractions |
| 13 | **abstraction** | conceptual/theoretical framing |
| 14 | **emotional_intensity** | arousal level of language |
| 15 | **humor** | deliberate comedic elements |
| 16 | **self_reference** | “I / me / my” frequency |
| 17 | **audience_engagement** | “you / we / let’s” usage |
| 18 | **interrogative_frequency** | questions posed |
| 19 | **exclamatory_frequency** | “!” usage |
| 20 | **passive_voice_usage** | % passive constructions |
| 21 | **metaphor_usage** | figurative comparisons |
| 22 | **irony** | meaning opposite to literal words |
| 23 | **domain_specificity** | focus on a niche field |
| 24 | **citation_frequency** | explicit references/links |
| 25 | **uncertainty_expression** | “might / could / perhaps” density |
| 26 | **decisiveness** | definitive statements |
| 27 | **numerical_precision** | exact numbers vs. vagueness |
| 28 | **ideological_intensity** | overt value signaling |
| 29 | **sensationalism** | dramatic framing |
| 30 | **bias_visibility** | transparent partiality |
| 31 | **empathy** | concern for others’ feelings |
| 32 | **skepticism** | critical scrutiny |
| 33 | **conspiratorial_tone** | hidden-agenda framing |
| 34 | **nationalism_tone** | nation-centric pride |
| 35 | **globalization_tone** | cosmopolitan framing |
| 36 | **technophilia** | enthusiasm for technology |
| 37 | **environmental_concern** | ecological focus |
| 38 | **humanitarian_focus** | human-rights framing |
| 39 | **corporate_focus** | business/market emphasis |
| 40 | **government_trust** | deference to state institutions |
| 41 | **institutional_trust** | trust in academia, NGOs, etc. |
| 42 | **data_driven_reasoning** | reliance on quantitative proof |
| 43 | **narrative_storytelling** | anecdotal/plot framing |
| 44 | **historical_context_usage** | references to past events |
| 45 | **future_projection** | forecasting / futurology |
| 46 | **complexity** | syntactic difficulty |
| 47 | **brevity** | concision (inverse of verbosity) |
| 48 | **redundancy** | repetition of ideas |
| 49 | **rhetorical_devices** | alliteration, triads, etc. |
| 50 | **clarity** | ease of comprehension |
---
## 2 · Three sample persona YAMLs
Copy each block into its own file (e.g., `establishment_analyst.yaml`, `grassroots_populist.yaml`, `techno_green_progressive.yaml`).
All keys correspond to the 50 measures above; values are illustrative 0 – 1 scalars.
### 📄 `establishment_analyst.yaml`
```yaml
formality: 0.8
verbosity: 0.6
lexical_diversity: 0.75
sentimentality: 0.30
optimism: 0.55
sarcasm: 0.10
politeness: 0.70
assertiveness: 0.60
hedging: 0.65
evidence_density: 0.85
jargon_level: 0.70
concreteness: 0.65
abstraction: 0.55
emotional_intensity: 0.25
humor: 0.15
self_reference: 0.20
audience_engagement: 0.40
interrogative_frequency: 0.30
exclamatory_frequency: 0.05
passive_voice_usage: 0.40
metaphor_usage: 0.35
irony: 0.10
domain_specificity: 0.70
citation_frequency: 0.80
uncertainty_expression: 0.50
decisiveness: 0.55
numerical_precision: 0.85
ideological_intensity: 0.30
sensationalism: 0.10
bias_visibility: 0.20
empathy: 0.45
skepticism: 0.40
conspiratorial_tone: 0.05
nationalism_tone: 0.30
globalization_tone: 0.60
technophilia: 0.55
environmental_concern: 0.50
humanitarian_focus: 0.45
corporate_focus: 0.60
government_trust: 0.65
institutional_trust: 0.70
data_driven_reasoning: 0.90
narrative_storytelling: 0.40
historical_context_usage: 0.60
future_projection: 0.50
complexity: 0.75
brevity: 0.35
redundancy: 0.25
rhetorical_devices: 0.40
clarity: 0.85
```
---
### 📄 `grassroots_populist.yaml`
```yaml
formality: 0.25
verbosity: 0.70
lexical_diversity: 0.55
sentimentality: 0.70
optimism: 0.40
sarcasm: 0.55
politeness: 0.30
assertiveness: 0.80
hedging: 0.20
evidence_density: 0.30
jargon_level: 0.25
concreteness: 0.75
abstraction: 0.30
emotional_intensity: 0.80
humor: 0.60
self_reference: 0.50
audience_engagement: 0.75
interrogative_frequency: 0.60
exclamatory_frequency: 0.70
passive_voice_usage: 0.20
metaphor_usage: 0.70
irony: 0.50
domain_specificity: 0.35
citation_frequency: 0.20
uncertainty_expression: 0.25
decisiveness: 0.80
numerical_precision: 0.20
ideological_intensity: 0.70
sensationalism: 0.80
bias_visibility: 0.70
empathy: 0.60
skepticism: 0.75
conspiratorial_tone: 0.60
nationalism_tone: 0.65
globalization_tone: 0.20
technophilia: 0.35
environmental_concern: 0.40
humanitarian_focus: 0.50
corporate_focus: 0.20
government_trust: 0.20
institutional_trust: 0.15
data_driven_reasoning: 0.25
narrative_storytelling: 0.75
historical_context_usage: 0.50
future_projection: 0.45
complexity: 0.45
brevity: 0.40
redundancy: 0.55
rhetorical_devices: 0.70
clarity: 0.60
```
---
### 📄 `techno_green_progressive.yaml`
```yaml
formality: 0.60
verbosity: 0.65
lexical_diversity: 0.80
sentimentality: 0.50
optimism: 0.85
sarcasm: 0.20
politeness: 0.65
assertiveness: 0.55
hedging: 0.35
evidence_density: 0.70
jargon_level: 0.60
concreteness: 0.60
abstraction: 0.70
emotional_intensity: 0.45
humor: 0.40
self_reference: 0.25
audience_engagement: 0.55
interrogative_frequency: 0.40
exclamatory_frequency: 0.15
passive_voice_usage: 0.35
metaphor_usage: 0.50
irony: 0.25
domain_specificity: 0.60
citation_frequency: 0.75
uncertainty_expression: 0.40
decisiveness: 0.60
numerical_precision: 0.70
ideological_intensity: 0.50
sensationalism: 0.25
bias_visibility: 0.30
empathy: 0.70
skepticism: 0.35
conspiratorial_tone: 0.10
nationalism_tone: 0.25
globalization_tone: 0.55
technophilia: 0.85
environmental_concern: 0.85
humanitarian_focus: 0.65
corporate_focus: 0.30
government_trust: 0.50
institutional_trust: 0.50
data_driven_reasoning: 0.75
narrative_storytelling: 0.55
historical_context_usage: 0.55
future_projection: 0.80
complexity: 0.65
brevity: 0.40
redundancy: 0.30
rhetorical_devices: 0.45
clarity: 0.80
```
---
### Next steps
*Plug these YAML personas into your `persona_writer.py` workflow (or any other analysis pipeline) to generate or critique news content from three sharply different angles.* Fine-tune the 0–1 values as you gather real output and analytics.
gpt-4o
now rewrite this prompt so that it uses fstring to fill in a prompt which describes the variations representing through using a formed prompt that uses the quantitative values from the .yaml persona file to fill in the variables in the fstring instead of {traits_desc} : prompt = (
f"You are an AI assistant emulating a writer with the following traits: {traits_desc}.\n"
f"Write the following text in this style:\n\n{base_text}"
)
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Below is a drop-in replacement for `generate_prompt()` that:
1. **Loads** the 50 style measures from the YAML persona (`persona["traits"]`).
2. **Exposes** each measure as a local variable so an f-string can reference it directly.
3. **Builds** a multi-line prompt that prints every trait on its own line (no more single `{traits_desc}` blob).
```python
import textwrap
def generate_prompt(base_text: str, persona: dict) -> str:
"""
Construct an LLM prompt that spells out **each** quantitative style measure
using f-string interpolation, rather than a single comma-separated list.
"""
# Pull the numerical traits into local variables so the f-string can see them
locals().update(persona["traits"])
prompt = f"""
You are an AI assistant emulating a writer whose quantitative style metrics are:
- Formality: {formality:.2f}
- Verbosity: {verbosity:.2f}
- Lexical Diversity: {lexical_diversity:.2f}
- Sentimentality: {sentimentality:.2f}
- Optimism: {optimism:.2f}
- Sarcasm: {sarcasm:.2f}
- Politeness: {politeness:.2f}
- Assertiveness: {assertiveness:.2f}
- Hedging: {hedging:.2f}
- Evidence Density: {evidence_density:.2f}
- Jargon Level: {jargon_level:.2f}
- Concreteness: {concreteness:.2f}
- Abstraction: {abstraction:.2f}
- Emotional Intensity: {emotional_intensity:.2f}
- Humor: {humor:.2f}
- Self-Reference: {self_reference:.2f}
- Audience Engagement: {audience_engagement:.2f}
- Interrogative Freq.: {interrogative_frequency:.2f}
- Exclamatory Freq.: {exclamatory_frequency:.2f}
- Passive Voice Usage: {passive_voice_usage:.2f}
- Metaphor Usage: {metaphor_usage:.2f}
- Irony: {irony:.2f}
- Domain Specificity: {domain_specificity:.2f}
- Citation Frequency: {citation_frequency:.2f}
- Uncertainty Expr.: {uncertainty_expression:.2f}
- Decisiveness: {decisiveness:.2f}
- Numerical Precision: {numerical_precision:.2f}
- Ideological Intensity: {ideological_intensity:.2f}
- Sensationalism: {sensationalism:.2f}
- Bias Visibility: {bias_visibility:.2f}
- Empathy: {empathy:.2f}
- Skepticism: {skepticism:.2f}
- Conspiratorial Tone: {conspiratorial_tone:.2f}
- Nationalism Tone: {nationalism_tone:.2f}
- Globalization Tone: {globalization_tone:.2f}
- Technophilia: {technophilia:.2f}
- Environmental Concern: {environmental_concern:.2f}
- Humanitarian Focus: {humanitarian_focus:.2f}
- Corporate Focus: {corporate_focus:.2f}
- Government Trust: {government_trust:.2f}
- Institutional Trust: {institutional_trust:.2f}
- Data-Driven Reasoning: {data_driven_reasoning:.2f}
- Narrative Storytelling: {narrative_storytelling:.2f}
- Historical Context: {historical_context_usage:.2f}
- Future Projection: {future_projection:.2f}
- Complexity: {complexity:.2f}
- Brevity: {brevity:.2f}
- Redundancy: {redundancy:.2f}
- Rhetorical Devices: {rhetorical_devices:.2f}
- Clarity: {clarity:.2f}
Using exactly those stylistic parameters, write the following text:
{base_text}
"""
# Clean up leading whitespace for neatness
return textwrap.dedent(prompt).lstrip()
```
### How it works
- `locals().update(persona["traits"])` copies the 50 keys into local scope, so `{formality:.2f}`, `{verbosity:.2f}`, etc. are all valid placeholders in the f-string.
- `textwrap.dedent` trims the indentation introduced by the triple-quoted string for a clean prompt.
- The resulting prompt now **explicitly spells out every quantitative slider** the model should honor, making personas transparent and easily auditable.
gpt-4o
now rewrite this function as well : def analyze_text_traits(text, model=OLLAMA_MODEL):
analysis_prompt = (
"Analyze the following text and provide a JSON with scores from 0 to 1 "
"for the traits: introversion, creativity, cynicism, optimism, formality, "
"verbosity, humor, skepticism.\n\n"
f"Text:\n\"\"\"\n{text}\n\"\"\""
)
raw = call_llm_ollama(analysis_prompt, model)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"error": "Failed to parse JSON from response", "raw": raw}
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Here’s a drop-in replacement that asks the model to score **all 50 quantitative measures** (pulled from a single list so you keep one source of truth). It returns a parsed-JSON dict or a clear error object if parsing fails.
```python
# Put your canonical list of 50 keys in ONE place:
TRAIT_KEYS = [
"formality", "verbosity", "lexical_diversity", "sentimentality", "optimism",
"sarcasm", "politeness", "assertiveness", "hedging", "evidence_density",
"jargon_level", "concreteness", "abstraction", "emotional_intensity", "humor",
"self_reference", "audience_engagement", "interrogative_frequency",
"exclamatory_frequency", "passive_voice_usage", "metaphor_usage", "irony",
"domain_specificity", "citation_frequency", "uncertainty_expression",
"decisiveness", "numerical_precision", "ideological_intensity",
"sensationalism", "bias_visibility", "empathy", "skepticism",
"conspiratorial_tone", "nationalism_tone", "globalization_tone",
"technophilia", "environmental_concern", "humanitarian_focus",
"corporate_focus", "government_trust", "institutional_trust",
"data_driven_reasoning", "narrative_storytelling", "historical_context_usage",
"future_projection", "complexity", "brevity", "redundancy",
"rhetorical_devices", "clarity"
]
def analyze_text_traits(text: str, model: str = OLLAMA_MODEL, traits: list[str] = TRAIT_KEYS) -> dict:
"""
Ask the local Ollama model to score `text` on every trait in `traits`
(0.0–1.0). Returns a Python dict parsed from the model’s JSON response.
"""
trait_list = ", ".join(traits)
analysis_prompt = (
f"Analyze the following text and reply with a VALID JSON object whose keys are:\n"
f"{trait_list}\n\n"
"Each key's value must be a float between 0 and 1 representing how strongly "
"that trait is present.\n\n"
f'Text:\n"""\n{text}\n"""'
)
raw = call_llm_ollama(analysis_prompt, model)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {
"error": "Failed to parse JSON from model response",
"raw_response": raw
}
```
### What changed
- **Single source of trait names** via `TRAIT_KEYS`.
- The prompt now enumerates all 50 keys dynamically (so edits propagate automatically).
- Clear error object if the LLM returns malformed JSON.
gpt-4o
import argparse
import requests # still convenient for the local Ollama REST API
import textwrap
import yaml # Import the PyYAML library
OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "mistral-small3.2:latest"
# -------------------------------
# Persona Utilities
# -------------------------------
def save_persona(persona, filename):
with open(filename, "w") as f:
yaml.dump(persona, f, indent=2, sort_keys=False) # Use yaml.dump
def load_persona(filename):
with open(filename) as f:
return yaml.safe_load(f) # Use yaml.safe_load
# -------------------------------
# Prompt Generator
# -------------------------------
def generate_prompt(base_text: str, persona: dict) -> str:
"""
Construct an LLM prompt that spells out **each** quantitative style measure
using f-string interpolation, rather than a single comma-separated list.
"""
# Pull the numerical traits into local variables so the f-string can see them
locals().update(persona["traits"])
prompt = f"""
You are an AI assistant emulating a writer whose quantitative style metrics are:
- Formality: {formality:.2f}
- Verbosity: {verbosity:.2f}
- Lexical Diversity: {lexical_diversity:.2f}
- Sentimentality: {sentimentality:.2f}
- Optimism: {optimism:.2f}
- Sarcasm: {sarcasm:.2f}
- Politeness: {politeness:.2f}
- Assertiveness: {assertiveness:.2f}
- Hedging: {hedging:.2f}
- Evidence Density: {evidence_density:.2f}
- Jargon Level: {jargon_level:.2f}
- Concreteness: {concreteness:.2f}
- Abstraction: {abstraction:.2f}
- Emotional Intensity: {emotional_intensity:.2f}
- Humor: {humor:.2f}
- Self-Reference: {self_reference:.2f}
- Audience Engagement: {audience_engagement:.2f}
- Interrogative Freq.: {interrogative_frequency:.2f}
- Exclamatory Freq.: {exclamatory_frequency:.2f}
- Passive Voice Usage: {passive_voice_usage:.2f}
- Metaphor Usage: {metaphor_usage:.2f}
- Irony: {irony:.2f}
- Domain Specificity: {domain_specificity:.2f}
- Citation Frequency: {citation_frequency:.2f}
- Uncertainty Expr.: {uncertainty_expression:.2f}
- Decisiveness: {decisiveness:.2f}
- Numerical Precision: {numerical_precision:.2f}
- Ideological Intensity: {ideological_intensity:.2f}
- Sensationalism: {sensationalism:.2f}
- Bias Visibility: {bias_visibility:.2f}
- Empathy: {empathy:.2f}
- Skepticism: {skepticism:.2f}
- Conspiratorial Tone: {conspiratorial_tone:.2f}
- Nationalism Tone: {nationalism_tone:.2f}
- Globalization Tone: {globalization_tone:.2f}
- Technophilia: {technophilia:.2f}
- Environmental Concern: {environmental_concern:.2f}
- Humanitarian Focus: {humanitarian_focus:.2f}
- Corporate Focus: {corporate_focus:.2f}
- Government Trust: {government_trust:.2f}
- Institutional Trust: {institutional_trust:.2f}
- Data-Driven Reasoning: {data_driven_reasoning:.2f}
- Narrative Storytelling: {narrative_storytelling:.2f}
- Historical Context: {historical_context_usage:.2f}
- Future Projection: {future_projection:.2f}
- Complexity: {complexity:.2f}
- Brevity: {brevity:.2f}
- Redundancy: {redundancy:.2f}
- Rhetorical Devices: {rhetorical_devices:.2f}
- Clarity: {clarity:.2f}
Using exactly those stylistic parameters, write the following text:
{base_text}
"""
# Clean up leading whitespace for neatness
return textwrap.dedent(prompt).lstrip()
# -------------------------------
# LLM Call (Ollama local)
# -------------------------------
def call_llm_ollama(prompt, model=OLLAMA_MODEL, stream=False):
"""
Generate a single completion with the local Ollama server.
"""
payload = {
"model": model,
"prompt": prompt,
"stream": stream # we'll keep streaming off for simplicity
}
resp = requests.post(OLLAMA_URL, json=payload)
resp.raise_for_status()
data = resp.json()
return data["response"]
# -------------------------------
# Trait Analyzer (also via Ollama)
# -------------------------------
# Put your canonical list of 50 keys in ONE place:
TRAIT_KEYS = [
"formality", "verbosity", "lexical_diversity", "sentimentality", "optimism",
"sarcasm", "politeness", "assertiveness", "hedging", "evidence_density",
"jargon_level", "concreteness", "abstraction", "emotional_intensity", "humor",
"self_reference", "audience_engagement", "interrogative_frequency",
"exclamatory_frequency", "passive_voice_usage", "metaphor_usage", "irony",
"domain_specificity", "citation_frequency", "uncertainty_expression",
"decisiveness", "numerical_precision", "ideological_intensity",
"sensationalism", "bias_visibility", "empathy", "skepticism",
"conspiratorial_tone", "nationalism_tone", "globalization_tone",
"technophilia", "environmental_concern", "humanitarian_focus",
"corporate_focus", "government_trust", "institutional_trust",
"data_driven_reasoning", "narrative_storytelling", "historical_context_usage",
"future_projection", "complexity", "brevity", "redundancy",
"rhetorical_devices", "clarity"
]
def analyze_text_traits(text: str, model: str = OLLAMA_MODEL, traits: list[str] = TRAIT_KEYS) -> dict:
"""
Ask the local Ollama model to score `text` on every trait in `traits`
(0.0–1.0). Returns a Python dict parsed from the model’s JSON response.
"""
trait_list = ", ".join(traits)
analysis_prompt = (
f"Analyze the following text and reply with a VALID YAML object whose keys are:\n"
f"{trait_list}\n\n"
"Each key's value must be a float between 0 and 1 representing how strongly "
"that trait is present.\n\n"
f'Text:\n"""\n{text}\n"""'
)
raw = call_llm_ollama(analysis_prompt, model)
try:
return json.loads(raw)
except json.JSONDecodeError:
return {
"error": "Failed to parse JSON from model response",
"raw_response": raw
}
# -------------------------------
# Main CLI
# -------------------------------
def main():
parser = argparse.ArgumentParser(description="Persona AI Content Generator (Ollama edition)")
parser.add_argument("--persona", required=True, help="Path to persona JSON file")
parser.add_argument("--text", required=True, help="Seed text for generation")
parser.add_argument("--analyze", action="store_true", help="Also score the generated text for traits")
args = parser.parse_args()
persona = load_persona(args.persona)
prompt = generate_prompt(args.text, persona)
generated_text = call_llm_ollama(prompt)
print("\n=== Generated Text ===\n")
print(generated_text)
if args.analyze:
traits = analyze_text_traits(generated_text)
print("\n=== Trait Analysis ===\n")
print(json.dumps(traits, indent=2))
if __name__ == "__main__":
main()
⚙ model reasoning step
gpt-4o
⚙ model reasoning step
gpt-4o
Here are the **two fixes** you need so the script is internally consistent:
| Issue | Fix |
|-------|-----|
| `analyze_text_traits()` asks the model for **YAML**, yet tries to parse it with `json.loads()` | Parse with **`yaml.safe_load()`** instead (keep a `json_fallback` in case the model still returns JSON). |
| The CLI prints the analysis via `json.dumps()` | Dump it with `yaml.dump()` instead, so the output format matches what the model just produced. |
---
### 1 · Replace **`analyze_text_traits()`**
```python
def analyze_text_traits(
text: str,
model: str = OLLAMA_MODEL,
traits: list[str] = TRAIT_KEYS
) -> dict:
"""
Ask the local Ollama model to score `text` on every trait in `traits`
(0.0–1.0). Expects YAML from the model and returns a Python dict.
"""
trait_list = ", ".join(traits)
analysis_prompt = (
f"Analyze the following text and reply with a VALID YAML object whose keys are:\n"
f"{trait_list}\n\n"
"Each key's value must be a float between 0 and 1 representing how strongly "
"that trait is present.\n\n"
f'Text:\n"""\n{text}\n"""'
)
raw = call_llm_ollama(analysis_prompt, model)
# Try YAML first …
try:
return yaml.safe_load(raw)
except yaml.YAMLError:
# … but fall back to JSON if the model forgot and returned JSON.
try:
import json
return json.loads(raw)
except json.JSONDecodeError:
return {
"error": "Failed to parse YAML or JSON from model response",
"raw_response": raw
}
```
---
### 2 · Print the analysis as YAML
```python
if args.analyze:
traits = analyze_text_traits(generated_text)
print("\n=== Trait Analysis ===\n")
# Pretty-print in YAML so the user sees the same format
print(yaml.dump(traits, sort_keys=False, default_flow_style=False))
```
---
That’s it—no other sections need to change.
Run the script again and you’ll get:
```
=== Generated Text ===
... (LLM output) ...
=== Trait Analysis ===
formality: 0.78
verbosity: 0.63
...
clarity: 0.84
```
Enjoy the cleaner, format-consistent workflow!
gpt-4o
output the final file
Here is the complete and final version of your script, with all the requested functionality, including:
- YAML-based persona loading and saving
- Fully explicit f-string prompt with all 50 quantitative traits
- Analysis returning and printing YAML
- Local Ollama call using `mistral-small3.2:latest`
---
### 📄 `persona_writer.py`
```python
import argparse
import requests
import textwrap
import yaml
OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "mistral-small3.2:latest"
# -------------------------------
# Trait Keys (Single Source of Truth)
# -------------------------------
TRAIT_KEYS = [
"formality", "verbosity", "lexical_diversity", "sentimentality", "optimism",
"sarcasm", "politeness", "assertiveness", "hedging", "evidence_density",
"jargon_level", "concreteness", "abstraction", "emotional_intensity", "humor",
"self_reference", "audience_engagement", "interrogative_frequency",
"exclamatory_frequency", "passive_voice_usage", "metaphor_usage", "irony",
"domain_specificity", "citation_frequency", "uncertainty_expression",
"decisiveness", "numerical_precision", "ideological_intensity",
"sensationalism", "bias_visibility", "empathy", "skepticism",
"conspiratorial_tone", "nationalism_tone", "globalization_tone",
"technophilia", "environmental_concern", "humanitarian_focus",
"corporate_focus", "government_trust", "institutional_trust",
"data_driven_reasoning", "narrative_storytelling", "historical_context_usage",
"future_projection", "complexity", "brevity", "redundancy",
"rhetorical_devices", "clarity"
]
# -------------------------------
# Persona Utilities
# -------------------------------
def save_persona(persona, filename):
with open(filename, "w") as f:
yaml.dump(persona, f, indent=2, sort_keys=False)
def load_persona(filename):
with open(filename) as f:
return yaml.safe_load(f)
# -------------------------------
# Prompt Generator
# -------------------------------
def generate_prompt(base_text: str, persona: dict) -> str:
"""
Construct an LLM prompt that spells out **each** quantitative style measure
using f-string interpolation.
"""
locals().update(persona["traits"])
prompt = f"""
You are an AI assistant emulating a writer whose quantitative style metrics are:
- Formality: {formality:.2f}
- Verbosity: {verbosity:.2f}
- Lexical Diversity: {lexical_diversity:.2f}
- Sentimentality: {sentimentality:.2f}
- Optimism: {optimism:.2f}
- Sarcasm: {sarcasm:.2f}
- Politeness: {politeness:.2f}
- Assertiveness: {assertiveness:.2f}
- Hedging: {hedging:.2f}
- Evidence Density: {evidence_density:.2f}
- Jargon Level: {jargon_level:.2f}
- Concreteness: {concreteness:.2f}
- Abstraction: {abstraction:.2f}
- Emotional Intensity: {emotional_intensity:.2f}
- Humor: {humor:.2f}
- Self-Reference: {self_reference:.2f}
- Audience Engagement: {audience_engagement:.2f}
- Interrogative Freq.: {interrogative_frequency:.2f}
- Exclamatory Freq.: {exclamatory_frequency:.2f}
- Passive Voice Usage: {passive_voice_usage:.2f}
- Metaphor Usage: {metaphor_usage:.2f}
- Irony: {irony:.2f}
- Domain Specificity: {domain_specificity:.2f}
- Citation Frequency: {citation_frequency:.2f}
- Uncertainty Expr.: {uncertainty_expression:.2f}
- Decisiveness: {decisiveness:.2f}
- Numerical Precision: {numerical_precision:.2f}
- Ideological Intensity: {ideological_intensity:.2f}
- Sensationalism: {sensationalism:.2f}
- Bias Visibility: {bias_visibility:.2f}
- Empathy: {empathy:.2f}
- Skepticism: {skepticism:.2f}
- Conspiratorial Tone: {conspiratorial_tone:.2f}
- Nationalism Tone: {nationalism_tone:.2f}
- Globalization Tone: {globalization_tone:.2f}
- Technophilia: {technophilia:.2f}
- Environmental Concern: {environmental_concern:.2f}
- Humanitarian Focus: {humanitarian_focus:.2f}
- Corporate Focus: {corporate_focus:.2f}
- Government Trust: {government_trust:.2f}
- Institutional Trust: {institutional_trust:.2f}
- Data-Driven Reasoning: {data_driven_reasoning:.2f}
- Narrative Storytelling: {narrative_storytelling:.2f}
- Historical Context: {historical_context_usage:.2f}
- Future Projection: {future_projection:.2f}
- Complexity: {complexity:.2f}
- Brevity: {brevity:.2f}
- Redundancy: {redundancy:.2f}
- Rhetorical Devices: {rhetorical_devices:.2f}
- Clarity: {clarity:.2f}
Using exactly those stylistic parameters, write the following text:
{base_text}
"""
return textwrap.dedent(prompt).lstrip()
# -------------------------------
# LLM Call (Ollama local)
# -------------------------------
def call_llm_ollama(prompt, model=OLLAMA_MODEL, stream=False):
payload = {
"model": model,
"prompt": prompt,
"stream": stream
}
resp = requests.post(OLLAMA_URL, json=payload)
resp.raise_for_status()
return resp.json()["response"]
# -------------------------------
# Trait Analyzer (YAML aware)
# -------------------------------
def analyze_text_traits(text: str, model: str = OLLAMA_MODEL, traits: list[str] = TRAIT_KEYS) -> dict:
trait_list = ", ".join(traits)
analysis_prompt = (
f"Analyze the following text and reply with a VALID YAML object whose keys are:\n"
f"{trait_list}\n\n"
"Each key's value must be a float between 0 and 1 representing how strongly "
"that trait is present.\n\n"
f'Text:\n"""\n{text}\n"""'
)
raw = call_llm_ollama(analysis_prompt, model)
try:
return yaml.safe_load(raw)
except yaml.YAMLError:
try:
import json
return json.loads(raw)
except json.JSONDecodeError:
return {
"error": "Failed to parse YAML or JSON from model response",
"raw_response": raw
}
# -------------------------------
# Main CLI
# -------------------------------
def main():
parser = argparse.ArgumentParser(description="Persona AI Content Generator (Ollama edition)")
parser.add_argument("--persona", required=True, help="Path to persona YAML file")
parser.add_argument("--text", required=True, help="Seed text for generation")
parser.add_argument("--analyze", action="store_true", help="Also score the generated text for traits")
args = parser.parse_args()
persona = load_persona(args.persona)
prompt = generate_prompt(args.text, persona)
generated_text = call_llm_ollama(prompt)
print("\n=== Generated Text ===\n")
print(generated_text)
if args.analyze:
traits = analyze_text_traits(generated_text)
print("\n=== Trait Analysis ===\n")
print(yaml.dump(traits, sort_keys=False, default_flow_style=False))
if __name__ == "__main__":
main()
```
---
### ✅ Summary
- ✅ Reads and writes personas in `.yaml`
- ✅ Generates stylistically-controlled prompts using 50 traits
- ✅ Calls **Ollama** with `mistral-small3.2:latest`
- ✅ Analyzes output style and prints YAML trait vector
Let me know if you want:
- GUI wrapper
- Auto-generate personas from text
- Multi-persona debates / comparisons
- Save generated text + traits to disk for training
gpt-4o