from dialoghelper.core import *
from solveit_dmtools import * # core, dhb, dhp, fab
import solveit_dmtools # This will be just for the next cell to print the versionprint(solveit_dmtools.__version__); del solveit_dmtools0.0.30
TL;DR:
| module | what it gives you | try |
|---|---|---|
dhb |
chat with any LiteLLM model you have a key for | bc = dhb.c('gemini/gemini-flash-lite-latest'); bc("hi") |
dhp |
Polya’s four stages as one-liners that write prompt cells | dhp.help(), dhp.u.summary() |
fab |
200+ open-source fabric prompts | fab.p.summarize() |
Full docs for each module are on its own page; await add_msg(modulename.doc, msg_type='note') prints more details in a cell if you’d rather stay here.
First calling with no model will prompt you to type in part of a model name to search
dhb.c() # `dhb.c()` with no model prompts you for a substring and lists matching modelsPlease try again by using e.g. `bc = dhb.c('model_name')` with a model name e.g. pick from these found by searching for 'gemini/gem':
gemini/gemini-live-2.5-flash-preview-native-audio-09-2025
gemini/gemini-robotics-er-1.5-preview
gemini/gemini-robotics-er-2-preview
gemini/gemini-robotics-er-1.6-preview
gemini/gemini-embedding-001
gemini/gemini-embedding-2-preview
gemini/gemini-embedding-2
gemini/gemini-1.5-flash
gemini/gemini-2.0-flash
gemini/gemini-2.0-flash-001
gemini/gemini-2.0-flash-lite
gemini/gemini-2.5-flash
gemini/gemini-2.5-flash-image
gemini/gemini-3-pro-image
gemini/gemini-3-pro-image-preview
gemini/gemini-3.1-flash-image
gemini/gemini-3.1-flash-image-preview
gemini/gemini-2.5-flash-lite
gemini/gemini-2.5-flash-lite-preview-09-2025
gemini/gemini-2.5-flash-preview-09-2025
gemini/gemini-flash-latest
gemini/gemini-flash-lite-latest
gemini/gemini-2.5-flash-lite-preview-06-17
gemini/gemini-2.5-flash-preview-tts
gemini/gemini-2.5-pro
gemini/gemini-2.5-computer-use-preview-10-2025
gemini/gemini-3-pro-preview
gemini/gemini-3.1-flash-lite-preview
gemini/gemini-3.1-flash-lite
gemini/gemini-3.5-flash-lite
gemini/gemini-3-flash-preview
gemini/gemini-3.5-flash
gemini/gemini-3.6-flash
gemini/gemini-omni-flash-preview
gemini/gemini-3.1-pro-preview
gemini/gemini-3.1-pro-preview-customtools
gemini/gemini-2.5-pro-preview-tts
gemini/gemini-exp-1114
gemini/gemini-exp-1206
gemini/gemini-gemma-2-27b-it
gemini/gemini-gemma-2-9b-it
gemini/gemma-3-27b-it
gemini/gemini-2.0-flash-exp-image-generation
gemini/gemini-2.0-flash-lite-001
gemini/gemini-2.5-flash-native-audio-latest
gemini/gemini-2.5-flash-native-audio-preview-09-2025
gemini/gemini-2.5-flash-native-audio-preview-12-2025
gemini/gemini-3.1-flash-live-preview
gemini/gemini-pro-latest
### The following ones are listed by OpenRouter but not LiteLLM (may still work)
<solveit_dmtools.dhb.BackupChat>
bc = dhb.c("openrouter/openai/gpt-5.2-codex")
# bc = dhb.c("openrouter/moonshotai/kimi-k2.5")The following will be automatically commented out when run, then a prompt cell is added after it with input/output from the other LLM.
bc("Hi, can you use tools?")Yes—I can use the available tools (e.g., read_url), but I’ll ask for confirmation before accessing any URLs. What would you like to do?
Details
- id:
gen-1786095705-Y8KGvpeB6uBr8yLeAjp2 - model:
openai/gpt-5.2-codex - finish_reason:
stop - usage:
Usage(completion_tokens=71, prompt_tokens=1738, total_tokens=1809, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=97, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=0, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0040355, is_byok=False, cost_details={'upstream_inference_cost': 0.0040355, 'upstream_inference_prompt_cost': 0.0030415, 'upstream_inference_completions_cost': 0.000994})
Prompt (openrouter/openai/gpt-5.2-codex): Hi, can you use tools?
Yes—I can use the available tools (e.g., read_url), but I’ll ask for confirmation before accessing any URLs. What would you like to do?
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 1738, "completion_tokens": 71, "total_tokens": 1809, "cached_tokens": 0, "cache_creation_tokens": 0, "cost": 0.0043995}def bad_joke():
"Tells a bad joke"
return "Why are engineers bad at telling jokes timing?"bc.add_tools('bad_joke')
len(bc.tool_schemas)2
%bc Tell me a bad joke using your tools pleaseWhy are engineers bad at telling jokes timing?
Details
- id:
gen-1786095857-BqlLTP3a2yngmpz7ZTGr - model:
openai/gpt-5.2-codex - finish_reason:
stop - usage:
Usage(completion_tokens=13, prompt_tokens=1922, total_tokens=1935, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=1792, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0007231, is_byok=False, cost_details={'upstream_inference_cost': 0.0007231, 'upstream_inference_prompt_cost': 0.0005411, 'upstream_inference_completions_cost': 0.000182})
Prompt (openrouter/openai/gpt-5.2-codex): Tell me a bad joke using your tools please
{
"id": "call_tXyLFSyyNoargmQNSStFu1c7",
"name": "bad_joke",
"args": {},
"result": "Why are engineers bad at telling jokes timing?"
}Why are engineers bad at telling jokes timing?
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 3811, "completion_tokens": 44, "total_tokens": 3855, "cached_tokens": 1792, "cache_creation_tokens": 0, "cost": 0.00446285}Prompt (openrouter/openai/gpt-5.2-codex): Tell me a bad joke using your tools please
I don’t have access to bad_joke in this session. Can you add it with bc.add_tools('bad_joke') (or bc.add_vars_and_tools) and then share it here?
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 1874, "completion_tokens": 114, "total_tokens": 1988, "cached_tokens": 1536, "cache_creation_tokens": 0, "cost": 0.0024563}bc("Can you please read https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md and give the elevator pitch and some sample code? No need to store the raw content.")Reading that URL is fine if you confirm. Want me to fetch it now with read_url?
Details
- id:
gen-1786095913-XosONBUH1ZYe2DxcSfAS - model:
openai/gpt-5.2-codex - finish_reason:
stop - usage:
Usage(completion_tokens=41, prompt_tokens=2650, total_tokens=2691, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=1664, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.0025907, is_byok=False, cost_details={'upstream_inference_cost': 0.0025907, 'upstream_inference_prompt_cost': 0.0020167, 'upstream_inference_completions_cost': 0.000574})
Prompt (openrouter/openai/gpt-5.2-codex): Can you please read https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md and give the elevator pitch and some sample code? No need to store the raw content.
Reading that URL is fine if you confirm. Want me to fetch it now with read_url?
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 2650, "completion_tokens": 41, "total_tokens": 2691, "cached_tokens": 1664, "cache_creation_tokens": 0, "cost": 0.0025907}The above is built-in to ask for confirmation… some models will not bother to ask each time, though.
%%bc
Yes that
is fineHere’s a concise summary based on the README you linked.
Elevator pitch
fhdaisy is a Python wrapper that maps DaisyUI components to FastHTML-friendly Python components. Instead of writing raw HTML with class strings (e.g., class="btn btn-primary"), you use components like Btn('Click me', cls='-primary'). Every DaisyUI class becomes a title‑case component, and modifiers can be shortened (btn-primary → -primary). There’s also an xtras module with helper builders (e.g., accordions) to reduce repetitive markup.
Sample code (minimal examples)
from fhdaisy import *
c = Btn("Hey there", cls="-primary")from fhdaisy import *
c = Alert("Saved!", cls="-success -soft")from fhdaisy.xtras import *
acc = mk_accordion(("Q1", "A1"), ("Q2", "A2"), itemcls="-arrow")Want a specific example (e.g., cards, forms, modals) or a tiny FastHTML page stub to see these components in context?
Details
- id:
gen-1786095942-VNg6t7CR0H1QZMAR9Fu4 - model:
openai/gpt-5.2-codex - finish_reason:
stop - usage:
Usage(completion_tokens=250, prompt_tokens=16259, total_tokens=16509, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=None, text_tokens=None, image_tokens=0, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=0, cache_write_tokens=0, cached_tokens=2944, text_tokens=None, image_tokens=None, video_tokens=0, cache_creation_tokens=0), cost=0.02731645, is_byok=False, cost_details={'upstream_inference_cost': 0.02731645, 'upstream_inference_prompt_cost': 0.02381645, 'upstream_inference_completions_cost': 0.0035})
Prompt (openrouter/openai/gpt-5.2-codex): Yes that is fine
{
"id": "call_8gSnY8pSbnzVTUt3UIPFo84Y",
"name": "read_url",
"args": {
"url": "https://raw.githubusercontent.com/AnswerDotAI/fhdaisy/refs/heads/main/README.md",
"as_md": true,
"extract_section": true,
"selector": "",
"ai_img": false
},
"result": "# fhdaisy\n\n\n<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->\n\n`fhdaisy` is a Python wrapp<TRUNCATED>"
}Here’s a concise summary based on the README you linked.
Elevator pitch
fhdaisy is a Python wrapper that maps DaisyUI components to FastHTML-friendly Python components. Instead of writing raw HTML with class strings (e.g., class="btn btn-primary"), you use components like Btn('Click me', cls='-primary'). Every DaisyUI class becomes a title‑case component, and modifiers can be shortened (btn-primary → -primary). There’s also an xtras module with helper builders (e.g., accordions) to reduce repetitive markup.
Sample code (minimal examples)
from fhdaisy import *
c = Btn("Hey there", cls="-primary")from fhdaisy import *
c = Alert("Saved!", cls="-success -soft")from fhdaisy.xtras import *
acc = mk_accordion(("Q1", "A1"), ("Q2", "A2"), itemcls="-arrow")Want a specific example (e.g., cards, forms, modals) or a tiny FastHTML page stub to see these components in context?
{"model": "openrouter/openai/gpt-5.2-codex", "prompt_tokens": 19266, "completion_tokens": 320, "total_tokens": 19586, "cached_tokens": 5376, "cache_creation_tokens": 0, "cost": 0.0297283}Dialog Helper for Polya’s Problem-Solving Method
The dhp module provides quick access to Polya’s four-stage problem-solving process through interactive prompts.
Each stage has multiple questions/prompts:
- To preview one you can just print it/type its name in a cell and hit Submit
- e.g.
dhp.act.nextshows you “(prompt) What is next?”. - To execute one you call it by adding () after the name
- e.g.
dhp.act.next()will replace the current message cell with a prompt cell having “What is next?” in it - It will be automatically executed, you can hit Esc to stop it and/or Enter to edit the prompt
TYPICAL FLOW:
- Start with
dhp.u(understand) (even briefly) to clarify your understanding of the problem - Move to
dhp.p(plan) to develop initial strategies - Switch between
planandact(ordhp.a, ordhp.x/execute) as you develop and test approaches - Use
dhp.r(review) to gain deeper understanding of your approach and findings - You might loop back to other steps after
dhp.r(review)
If you feel stuck, run dhp.help() and it will submit the prompt cell it creates - SolveIt will help you pick a next prompt!
The problem we want to solve is demonstrating the dhp module
The following cell is from running dhp.help() then running the prompt cell it creates
Please pick an appropriate next-step/prompt from the below:
Dialog Helper for Polya’s Problem-Solving Method
This module provides quick access to Polya’s four-stage problem-solving process through interactive prompts.
Each stage has multiple questions/prompts:
- To preview one you can just print it/type its name in a cell and hit Submit
- e.g.
dhp.act.nextshows you “(prompt) What is next?”. - To execute one you call it by adding () after the name
- e.g.
dhp.act.next()will replace the current message cell with a prompt cell having “What is next?” in it - It will be automatically executed, you can hit Esc to stop it and/or Enter to edit the prompt
TYPICAL FLOW:
- Start with
dhp.u(understand) (even briefly) to clarify your understanding of the problem - Move to
dhp.p(plan) to develop initial strategies - Switch between
planandact(ordhp.a, ordhp.x/execute) as you develop and test approaches - Use
dhp.r(review) to gain deeper understanding of your approach and findings - You might loop back to other steps after
r(review)
If you feel stuck, run dhp.help() and it will submit the prompt cell it creates - SolveIt will help you pick a next prompt!
UNDERSTAND STAGE - Clarify the problem before solving
- dhp.u.summary() - Creates a prompt cell asking SolveIt to give a concise summary of the problem
- dhp.u.info() - Creates a prompt cell asking SolveIt to inventory known/unknown information
- dhp.u.similar() - Creates a prompt cell asking SolveIt if it has seen a similar problem before
- dhp.u.lateral() - Creates a prompt cell to explore problem relationships and scope
- dhp.u.related() - Creates a prompt cell to identify similar or simpler problems
- dhp.u.viz() - Creates a prompt cell asking SolveIt to create a figure or diagram to represent the problem
- dhp.u.notation() - Creates a prompt cell asking SolveIt to pick suitable notation (symbols for quantities/data, states, transitions)
- dhp.u.simplest() - Creates a prompt cell asking SolveIt for the simplest way to look at the problem
- dhp.u.simplify() - Creates a prompt cell asking SolveIt to separate problem parts (break down complex conditions into simpler ones)
- dhp.u.objective() - Creates a note cell asking you to state the objective in one sentence, without reference to algorithms, models, or data
- dhp.u.levers() - Creates a note cell asking you to separate what you can control from what you cannot
- dhp.u.ideal_data() - Creates a prompt cell asking SolveIt what data you would collect if you could measure anything
PLAN STAGE - Develop strategies and approaches
- dhp.p.chunks() - Creates a prompt cell asking SolveIt to break down the problem into smaller sub-problems
- dhp.p.partial() - Creates a prompt cell asking SolveIt if there’s a smaller part or representation of the problem to solve
- dhp.p.known_approach() - Creates a prompt cell asking SolveIt to use a known algorithm or library to solve the problem
- dhp.p.verifiable() - Creates a prompt cell asking SolveIt how to verify if the solution is consistent and correct
- dhp.p.backward() - Creates a prompt cell asking SolveIt to work backward from the desired result
- dhp.p.aux() - Creates a prompt cell asking SolveIt to use an auxiliary element (variable, diagram, or example) to clarify the path
- dhp.p.analogy() - Creates a prompt cell asking SolveIt to use analogy or similarity to relate the problem to a known solution
- dhp.p.review() - Creates a prompt cell asking SolveIt to critique the plan of attack (be frank and critical)
- dhp.p.completeness() - Creates a prompt cell asking SolveIt if you’ve used all the data and taken into account all essential notions
ACT STAGE - Execute your plan while monitoring progress
- dhp.a.all() - Creates a prompt cell asking SolveIt if we covered all of the data or examples for this step
- dhp.a.check() - Creates a prompt cell asking SolveIt if this step seems correct
- dhp.a.doubt() - Creates a prompt cell asking SolveIt if we’re using the right approach
- dhp.a.next() - Creates a prompt cell asking SolveIt what is next
- dhp.a.other() - Creates a prompt cell asking SolveIt if there’s another way to look at this
- dhp.a.partial() - Creates a prompt cell asking SolveIt about intermediate results or milestones to aim for
- dhp.a.simpler() - Creates a prompt cell asking SolveIt if there was a simpler way to do this step
- dhp.a.symmetry() - Creates a prompt cell asking SolveIt about symmetries or patterns in the problem to exploit
- dhp.a.valid() - Creates a prompt cell asking SolveIt if this step was a valid step
REVIEW STAGE - Verify results, reflect on process, and extract lessons
- dhp.r.all() - Creates a prompt cell asking SolveIt if we covered all of the data or examples for this problem
- dhp.r.alter() - Creates a prompt cell asking SolveIt for alternative solutions or approaches that might be more efficient or effective
- dhp.r.general() - Creates a prompt cell asking SolveIt if we can generalize the solution to other similar problems
- dhp.r.grok() - Creates a note cell with the text “To consider: Can I understand the solution without having to perform all the steps?”
- dhp.r.learned() - Creates a prompt cell asking SolveIt what lessons have been learned from this
- dhp.r.mistakes() - Creates a prompt cell asking SolveIt about common mistakes made
- dhp.r.other() - Creates a prompt cell asking SolveIt if we can derive the result differently
- dhp.r.principles() - Creates a prompt cell asking SolveIt to identify underlying principles or patterns that emerged during the solution process
- dhp.r.sanity() - Creates a prompt cell asking SolveIt if the result makes sense and can be verified by substitution or another method
- dhp.r.simpler() - Creates a prompt cell asking SolveIt if we can derive the result in a simpler way
- dhp.r.test() - Creates a prompt cell asking SolveIt for different ways to test this
- dhp.r.framing() - Creates a note cell asking you to reflect on whether you started with the objective or with a tool
- dhp.r.levers_overlooked() - Creates a prompt cell asking SolveIt what levers you might have overlooked
- dhp.r.future_data() - Creates a prompt cell asking SolveIt what data you would collect differently next time
Since the stated problem is simply “demonstrating the dhp module,” the natural starting point is the understand stage. A good first step is dhp.u.summary() — it creates a prompt cell asking for a concise summary of the problem, which will also show what running one of these helpers actually looks like end-to-end.
dhp.u.summary(){"model": "claude-sonnet-5", "prompt_tokens": 60981, "completion_tokens": 219, "total_tokens": 61200, "cached_tokens": 8701, "cache_creation_tokens": 52278, "cost": 0.009311400000000001}# Uncomment and submit the line below if you do not already have a copy of fabric in your /app/data/fabric folder
#!cd /app/data; git clone --depth 1 https://github.com/danielmiessler/fabric.gitawait add_msg(content=fab.doc, msg_type="note")'_3719b4eb'
fab - Open Source ‘fabric’ prompts made quickly available in SolveIt
This module leverages over 200 open source LLM prompts that are available in Daniel Miesller’s ‘fabric’ project.
If you import as fab, Submit the following to see an overview of all the prompts:
fab.p
HOW TO USE IT
Most Common Syntax: prompt="Your Prompt" in one cell then fab.p.pattern_name() in another, where pattern_name is any of the 200+ available fabric patterns.
MOST IMPORTANT AND USED OPTIONS AND FEATURES
-
Variable Targeting: Use
fab.p.pattern_name('variable_name')to process content from a specific variable instead of the default ‘prompt’ variable. -
Pattern Discovery: Use
fab.p.help()(an alias for suggest_pattern()) to get suggestions of which pattern to pick for your prompt. -
Compression Feature: Use
fab.compress()after running a pattern to save tokens by marking the previous cell as skipped and compressing the output to a new note. -
Default Variable: Most patterns work with a variable called ‘prompt’ by default, making it easy to process your main content.
COMMON PATTERNS
- For Summarizing Content:
fab.p.summarize() - For Explaining Code:
fab.p.explain_code() - For Analyzing Claims:
fab.p.analyze_claims() - For Extracting Wisdom from Text:
fab.p.extract_wisdom() - For Creating Quizzes:
fab.p.create_quiz()
fab.p.explain_codeFabric pattern: explain_code - Analyze/explain code, security tool outputs, and configs.
prompt = read_url("https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py")The next cell is running fab.p.explain_code(), which we then run to get a result.
The call to any fab.p function also adds another cell below that has fab.compress(), which helps to reduce the tokens for later use.
(From fab.p.explain_code folded below) # IDENTITY and PURPOSE
You are an expert coder that takes code and documentation as input and do your best to explain it.
Take a deep breath and think step by step about how to best accomplish this goal using the following steps. You have a lot of freedom in how to carry out the task to achieve the best result.
-
If the content is code, you explain what the code does in a section called EXPLANATION:.
-
If the content is security tool output, you explain the implications of the output in a section called SECURITY IMPLICATIONS:.
-
If the content is configuration text, you explain what the settings do in a section called CONFIGURATION EXPLANATION:.
-
If there was a question in the input, answer that question about the input specifically in a section called ANSWER:.
- Do not output warnings or notes—just the requested sections.
INPUT:
$prompt
EXPLANATION:
This is a marimo notebook script (a reactive, cell-based Python UI, run with uv run given the inline PEP 723 metadata block at the top) that turns a web article into a two-host podcast, complete with synthesized audio.
Setup cells
- Declares dependencies (
marimo,gaspare,openai,pydub,contextkit) via the# ///scriptheader, souv runcan provision them automatically. - Imports
OpenAIfor text-to-speech,gaspare(aliasedgp) for calling an LLM (Gemini) to write the script,contextkit.read_urlto fetch and clean web content, andpydub.AudioSegmentfor stitching audio clips together. - Asserts that
OPENAI_API_KEYandGEMINI_API_KEYare set in the environment, then creates anOpenAIclient.
UI form
- Builds a marimo form with: source URL, output filename, two host names, two OpenAI TTS voice names, a slider for approximate episode length (3–60 min), and a “Run!” button. Nothing downstream executes until the button is clicked (
mo.stop(not run_button.value)gates every later cell).
Helper functions
clean_text_for_tts: repairs common mis-encoded punctuation (broken ellipses, curly quotes), strips non-ASCII characters, collapses whitespace, and NFKD-normalizes the text so it’s safe to feed to a TTS engine.parse_podcast_script: parses a raw script into a list of{"speaker": ..., "text": ...}segments, splitting on lines that start withHOSTNAME:(case-insensitive match against the two configured host names) and cleaning each segment’s text.
Pipeline (only runs once “Run!” is clicked)
read_urlfetches and converts the source page into text/markdown.- A large prompt is built asking Gemini to convert that source material into a two-host podcast script, with detailed instructions on hosts’ personalities, structure, tone, and a strict
HOSTNAME: textoutput format, targeting the requested approximate spoken length. gp.Chat(model="gemini-2.5-pro-preview-03-25")sends that prompt and the response is parsed intosegmentsviaparse_podcast_script.- For each segment,
client.audio.speech.with_streaming_response.create(OpenAI’sgpt-4o-mini-ttsmodel) synthesizes audio using the voice and persona instructions matching that speaker, streaming bytes into aBytesIObuffer and loading it as apydub.AudioSegment. - All segments are concatenated (
sum(parts, AudioSegment.empty())) into a singlepreviewaudio object, previewed inline, then exported to the configured output MP3 path. - The generated script text is also cleaned and written to a companion
_script.mdfile, and rendered inline as markdown for review.
Overall purpose: given nothing but a URL and some UI settings, the script scrapes an article, has an LLM turn it into a natural two-host dialogue script of a target length, and synthesizes it into a playable/downloadable podcast MP3 with two distinct AI voices — all inside an interactive marimo app.
{"model": "claude-sonnet-5", "prompt_tokens": 68864, "completion_tokens": 1259, "total_tokens": 70123, "cached_tokens": 8701, "cache_creation_tokens": 60161, "cost": 0.011273850000000002}EXPLANATION:
This is a marimo notebook script (a reactive, cell-based Python UI, run with uv run given the inline PEP 723 metadata block at the top) that turns a web article into a two-host podcast, complete with synthesized audio.
Setup cells
- Declares dependencies (
marimo,gaspare,openai,pydub,contextkit) via the# ///scriptheader, souv runcan provision them automatically. - Imports
OpenAIfor text-to-speech,gaspare(aliasedgp) for calling an LLM (Gemini) to write the script,contextkit.read_urlto fetch and clean web content, andpydub.AudioSegmentfor stitching audio clips together. - Asserts that
OPENAI_API_KEYandGEMINI_API_KEYare set in the environment, then creates anOpenAIclient.
UI form
- Builds a marimo form with: source URL, output filename, two host names, two OpenAI TTS voice names, a slider for approximate episode length (3–60 min), and a “Run!” button. Nothing downstream executes until the button is clicked (
mo.stop(not run_button.value)gates every later cell).
Helper functions
clean_text_for_tts: repairs common mis-encoded punctuation (broken ellipses, curly quotes), strips non-ASCII characters, collapses whitespace, and NFKD-normalizes the text so it’s safe to feed to a TTS engine.parse_podcast_script: parses a raw script into a list of{"speaker": ..., "text": ...}segments, splitting on lines that start withHOSTNAME:(case-insensitive match against the two configured host names) and cleaning each segment’s text.
Pipeline (only runs once “Run!” is clicked)
read_urlfetches and converts the source page into text/markdown.- A large prompt is built asking Gemini to convert that source material into a two-host podcast script, with detailed instructions on hosts’ personalities, structure, tone, and a strict
HOSTNAME: textoutput format, targeting the requested approximate spoken length. gp.Chat(model="gemini-2.5-pro-preview-03-25")sends that prompt and the response is parsed intosegmentsviaparse_podcast_script.- For each segment,
client.audio.speech.with_streaming_response.create(OpenAI’sgpt-4o-mini-ttsmodel) synthesizes audio using the voice and persona instructions matching that speaker, streaming bytes into aBytesIObuffer and loading it as apydub.AudioSegment. - All segments are concatenated (
sum(parts, AudioSegment.empty())) into a singlepreviewaudio object, previewed inline, then exported to the configured output MP3 path. - The generated script text is also cleaned and written to a companion
_script.mdfile, and rendered inline as markdown for review.
Overall purpose: given nothing but a URL and some UI settings, the script scrapes an article, has an LLM turn it into a natural two-host dialogue script of a target length, and synthesizes it into a playable/downloadable podcast MP3 with two distinct AI voices — all inside an interactive marimo app.
{"model": "claude-sonnet-5", "prompt_tokens": 68864, "completion_tokens": 1259, "total_tokens": 70123, "cached_tokens": 8701, "cache_creation_tokens": 60161, "cost": 0.011273850000000002}To go back full-circle, we can have a different LLM validate the results… Note that the %%bc cell magic allows you to pick a different BackupChat instance (%bc line magic does not)
gemini = dhb.c("gemini/gemini-3.1-pro-preview")%%bc gemini
Please compare the answer above to https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md and comment on any issues? It's OK, it's not a big download, so go ahead and read it!The previous code explanation was remarkably accurate and aligns perfectly with what the README.md describes! The explain_code prompt did a great job dissecting the pipeline (Contextkit -> Gemini 2.5 Pro -> OpenAI TTS -> Pydub) exactly as the author intended.
However, cross-referencing with the README.md reveals a few practical, user-facing details that the raw code analysis missed:
- Python Version Constraint: The README explicitly notes that you need a Python version before 3.13 (specifically recommending 3.12). This is due to a limitation with the
pydubaudio processing library. - Execution Method: While the code explanation noted it can be run via
uv rundue to the PEP 723 block, the README specifically recommends running it headlessly/sandboxed with:uvx marimo run --sandbox https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py - Patience Required: The README gives the user a heads-up that clicking “Run!” might take 6 to 10 minutes to process.
Are there any specific parts of the pipeline (like how it chunks the script or handles the TTS streams) you want to dive deeper into?
Details
- id:
OW14apiNOcyB-8YPgcW2mAI - model:
gemini-3.1-pro-preview - finish_reason:
stop - usage:
Usage(completion_tokens=1235, prompt_tokens=10316, total_tokens=11551, completion_tokens_details=CompletionTokensDetailsWrapper(accepted_prediction_tokens=None, audio_tokens=None, reasoning_tokens=948, rejected_prediction_tokens=None, text_tokens=287, image_tokens=None, video_tokens=None), prompt_tokens_details=PromptTokensDetailsWrapper(audio_tokens=None, cached_tokens=8059, text_tokens=2257, image_tokens=None, video_tokens=None), cache_read_input_tokens=8059)
Prompt (gemini/gemini-3.1-pro-preview): Please the answer above to https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md and comment on any issues? It’s OK, it’s not a big download, so go ahead and read it!
{
"id": "g4hepim7__thought__EswLCskLARFNMg8h/YeY1L8Iw6DSZ4ywSjFnmvqeqxTw5rQAWun4NunuJMsRwA4dSZ7dHOJ1lbKO6uZQG3z9Hp2y7/UGagi2YVINseLt7LSetvJBKvIJjobShhDo2Zz3N2B5SSMdlQ71bB7GbtICA8TVjaiU+IsH2nHJLElbBY6Mzi3UuHkniGQcEtay0BqY0D/sICEfa43WaEm698v/Y6CmGrGqZwDP441vxCTErekJBBd43A9p89ydbZTMawTm1KrnTMgnxEZcaz3PQFQq7AcbBekPe2bj6KMaPapRGa81SMMAZLjfZLqRp8fRerTR2e1PpC8as06L/r4yKSiyOxiirsLmHshJimIhwVItbUo6ToXpqBkIs4BZ1pZJ3/+Qa/lziyNvP9Ig6KCvroDFWeMSw1TNWGi9uepS9TAkwF/h8R9YtJVAJcV/BNcbdyKdH+8rc1lDPb6R+TettXIIYjnMRijYmCWjNgppx2+1QLOVoe/rNYN74ar0FfRFuWBlZ1g675W1V3vY6erVlN1CNiAiKrHvYZHPdx19phAQlJX+5yYxavQTmMpynfVOuLqUgOsSySxV52ZzdxH+ve5BGsrOMT4wk/aCgfOHhDJYMFOxnGWWmFUNOT5ncQbVPEi4QUwxRV3LBi6ubZhkiDM0NS7eC/OZc/6X8iLmdEuZlGWvMwb7NvXNBKnhfhpVz8q4GCXFy+Wbw4N5TqbP1mqKTVi7OP7dbR55zZ8+aCzJMsq/Zi4pCxRSApMt/kXd7yxawNTq+smzhnhWkCPHO8KntWJT1Eunv3b6xOqlVOqc7RW//tRli22skuUKjlyAqr7xcIGBNUsyfwNArQX79WSXT43VxJ8m+L/6+xjAvfgEHX9+MAEBbwctmwSAGyJyiR6DIOxp+iYpqdW2pp4rdvFcYyTkAOK9dITjGJFnEmriA02RD/zrPQdsV5lihcK4FsbJBqsi9Xl8cqkP4buAsAdshXeqO+URDfsyMngeRJo+4R5u424sEZRc5DI893/HTokU7XzaXmjil3NHyu+TbK3gD+aROxKu6Lz7TmS2C7d77RjaA4JxGPWDCY7CTpx01AmL23wZBxdarX9f/3/nnkV0A0O9mkvC+aSg4mriQbgsszRi9a945Eg4xGViTffeMWRs8m6VDssVEN80Hucj4K7vQ+alZ0nP67DpjaPD/EMKcrLXQmE/ymR+ZBzJHguGP6OYjLp91UByLUoCB7/B4kII9i3/OzVwu3XP0rNVZapNsURorvmOLlUTH2q9Zd5b6e2yHEOMkuPi3vL8iafm9FQ/Hptc3UspQXW6oB2WX6PCgeQD7u8TI84mvunKmh+BJJVqG/7BoaOJyIi54rOs+PzzirJuTGJtSJRqPOH5bGUk/eiZgunzPs79mShJKACzK0+I0+ZMBrhzWxs4+NX4KTCrTE8lbhC1yWyrMVSx/Oi5J8I9xhmHwfRr8oe9YE4Ykng371jZdHi8XaIN/gw++/Kgpg6IfPf1sTUPtg3gofGC3+l7T9k5CoOciPSjkUCzS54AAbVd6j9qPqOL3/mlRR4FTCKKR3yOrDZ+lVIUYNR4NC9QKFr1ThLkGvZRnZk/NXaM6QRauNTqAUbWe+ejL+BtDPhDt8V/8hPEqQLG8Q2u6I91SCQN2HOhwU/VsCmQHE50NJ56tmHxIfdrC1vYuJx3Z1cS+/+4hQ7jnKuIJcJcDX9F4PUfJZ6M7/3lB1KqjWy8UYAXzVxiNyuSrQ1wdZzuJ/iYjBwp42Lo58MRe2SRxSM4XZrAdm4eHyRM+gm0CO9/vYIhsTexPC+/XiQjRR1Xwrj2DH9m6ESD8zmeRasthKJfdsgzkA+SMDe5JjxI2Mu7cWaA060ib9V9ARSyqoYYzib6V7k47SAcyZF0Si2qZDQXPojv9i8bErRaupsLWCUSVjiELxVJVTigw+zvWSla/hkf+dFQFldKjf1qRkO0k/M4FKYCzrRnIQRJfIAbsEg=",
"name": "read_url",
"args": {
"url": "https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/README.md"
},
"result": "# blogtopod: Turn blog posts into podcast episodes\n\n*Convert blog posts into podcast episodes using <TRUNCATED>"
}The previous code explanation was remarkably accurate and aligns perfectly with what the README.md describes! The explain_code prompt did a great job dissecting the pipeline (Contextkit -> Gemini 2.5 Pro -> OpenAI TTS -> Pydub) exactly as the author intended.
However, cross-referencing with the README.md reveals a few practical, user-facing details that the raw code analysis missed:
- Python Version Constraint: The README explicitly notes that you need a Python version before 3.13 (specifically recommending 3.12). This is due to a limitation with the
pydubaudio processing library. - Execution Method: While the code explanation noted it can be run via
uv rundue to the PEP 723 block, the README specifically recommends running it headlessly/sandboxed with:uvx marimo run --sandbox https://raw.githubusercontent.com/shuane/blogtopod/refs/heads/main/blogtopod.py - Patience Required: The README gives the user a heads-up that clicking “Run!” might take 6 to 10 minutes to process.
Are there any specific parts of the pipeline (like how it chunks the script or handles the TTS streams) you want to dive deeper into?
{"model": "gemini/gemini-3.1-pro-preview", "prompt_tokens": 19260, "completion_tokens": 1657, "total_tokens": 20917, "cached_tokens": 8059, "cache_creation_tokens": 0, "cost": 0.0438978}If you are new to using nbdev here are some useful pointers to get you
started.
- local dev install:
$ pip install -e .
- make changes under nbs/ directory: …
- prepare for commit:
$ alias nbprep='nbdev-clean ; nbdev-export'
$ nbprep
#!pip install -e ..
# OR
#!pip install -Uqq solveit_dmtoolsInstall latest from the GitHub repository:
$ pip install git+https://github.com/shuane/solveit_dmtools.gitor from pypi
$ pip install solveit_dmtoolsDocumentation can be found hosted on this GitHub repository’s pages. Additionally you can find package manager specific guidelines on the pypi site.