Skip to content

durraniu/storytelling

Repository files navigation

storytelling

storytelling is an R package for creating fictional stories, illustrations, and narrations using AI models. This package provides functions to use various Web APIs to:

  • Generate fictional stories (generate_story())
  • Generate image prompts based on a story (generate_image_prompts())
  • Generate images based on the image prompts (generate_images())
  • Generate audios (speech) from story text (generate_audio())
  • Combine story text, images, and audio in a nice revealjs slide deck using Quarto (create_slides())

Under the hood, {storytime} uses {ellmer} and {httr2}.

Installation

You can install the development version of storytelling from GitHub with:

# install.packages("pak")
pak::pak("durraniu/storytelling")

Getting Started

You need Cloudflare account ID (CLOUDFLARE_ACCOUNT_ID) and API key (CLOUDFLARE_API_KEY) for generating images and narrations. Follow these instructions to get an account ID and an API key. Moreover, for generating story text, you need an API key for any provider that {ellmer} supports e.g., Open AI (OPENAI_API_KEY), Claude (ANTHROPIC_API_KEY), Google (GOOGLE_API_KEY), etc. Put all the keys and account ID in your .Renviron file.

You’d also need Quarto installed for creating revealjs slide decks when using the create_slides() function.

Example

Following is an example workflow to create a story slide deck. I set GOOGLE_API_KEY, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_KEY in my .Renviron file before running the code below.

Generate story text

In the generate_story() function, ellmer::chat_google_gemini and gemini-2.0-flash are the default chat function and model respectively. A title and five story paragraphs are generated by default:

library(storytelling)
# Provide a prompt for generating a story:
user_prompt <- "Tell me a story about a dragon who turned into a human."
story <- generate_story(user_prompt)
story$story
# [1] "Ignis, a dragon of the Crimson Peaks, lived a life of roaring flames and boundless skies. He reveled in his power, the terror he inspired, the mountains of gold he hoarded. Yet, a strange restlessness stirred within him, a longing for something beyond his dragon's existence. He saw the fleeting lives of humans in the valleys below, their laughter, their loves, their fleeting moments of joy and sorrow, and a desire bloomed within him to experience that ephemeral existence. He sought ancient magic, whispering through forgotten ruins, until he discovered a ritual promising transformation, a chance to walk among mortals, even if only for a while."
# [2] "The ritual was perilous, requiring a sacrifice of his most prized scale and the binding of his dragon heart. Underneath the pale glow of a lunar eclipse, Ignis chanted the ancient words. Agony ripped through him as his massive form shrunk and shifted. Scales turned to skin, claws to hands, wings to a strong but earthbound back. When the moon hid behind the clouds, Ignis stood as a man, naked and vulnerable, but with a heart pounding with a newfound excitement. He named himself Ash, a reminder of the fire he left behind."                                                                                                                             
# [3] "Ash descended into the valley, a stranger in a strange land. He quickly learned the ways of men, the nuances of their language, the complexities of their social structures. He found work as a blacksmith, his dragon-forged strength making him a master of the forge. He made friends, shared laughter, and even felt the pangs of love for a kind woman named Elara. He helped the villagers, using his knowledge of metalworking to create stronger tools and weapons, earning their respect and gratitude. He was no longer a terror, but a protector."                                                                                                              
# [4] "But the transformation was not without its price. The magic that bound him was fading, the dragon within stirring. Brief flashes of his former self would manifest – a sudden burst of heat, an uncontrollable urge to fly, a flicker of golden scales beneath his skin. He knew his time as Ash was limited. An old hermit, recognizing the dragon-man, warned him that his true form would eventually reclaim him, and the longer he resisted, the more destructive the change would be."                                                                                                                                                                                
# [5] "With a heavy heart, Ash bid farewell to Elara and his friends. He confessed his true nature, knowing they might fear him. Some did, but Elara saw the heart beneath the scales, the kindness that had defined his time among them. He climbed back to the Crimson Peaks, where, under the next lunar eclipse, he allowed the dragon to reclaim him. Ignis soared into the sky, the human experience forever etched into his heart, a changed dragon, more understanding, more compassionate, forever bound to the memory of his time as Ash, the blacksmith." 

Specify the number of paragraphs, genre and/or story structure:

The default values of genre and structure are “adventure” and “Hero’s Journey” respectively. You may generate stories of other genre e.g., “horror”, “scifi”, “mystery”, etc. In most cases the story structure of “Hero’s Journey” is good, but you may choose a different one. See the documentation for generate_story() for more options.

Furthermore, you can specify any integer for the number of paragraphs in the generated story in num_paras. Keep in mind that the num_paras should be a reasonable number so that the model’s max tokens are not exceeded. Here, we generate another story with 3 paragraphs and a different genre:

story_satire <- generate_story(user_prompt, num_paras = 3, genre = "satire")
story_satire$story
# [1] "Bartholomew the dragon, a connoisseur of the finest silks and a hoarder of vintage rubber duckies, lived a life of gilded boredom atop Mount Procrastination. One Tuesday, while lamenting the lack of decent artisanal cheese in his hoard, he stumbled upon an ancient scroll promising the ultimate adventure: humanity. Armed with a dubious potion and fueled by a desire to experience the complexities of reality television, Bartholomew transformed, emerging as Barry Higgins, an accountant with a penchant for beige."                                                                                                    
# [2] "Barry's new life was a whirlwind of spreadsheets and lukewarm coffee. He faced trials unlike any dragon: navigating office politics, understanding the offside rule, and mastering the art of microwaving popcorn without setting off the smoke alarm. His dragonish tendencies manifested in unexpected ways; a fascination with shiny staplers, an uncontrollable urge to nap after lunch, and a tendency to breathe a small puff of smoke when frustrated. Despite these challenges, Barry persevered, finding a strange sort of satisfaction in the mundane."                                                                     
# [3] "The climax arrived during the annual office potluck. Faced with a casserole of questionable origins, Barry, channeling his inner dragon, delivered a scathing critique of the dish's lack of seasoning, sparking a culinary revolution in the office. Returning to Mount Procrastination, Barry, now Bartholomew-Barry, a dragon with an appreciation for spreadsheets and a newfound respect for humanity's bizarre culinary creations, realized the true treasure wasn't gold or rubber duckies, but the absurdity of it all. He then immediately set up a corporation and began a hostile takeover of the artisanal cheese market."

Use a different provider

You may use different providers that are supported by {ellmer}. The following should work but not tested:

generate_story(user_prompt, 
               chat_fn = ellmer::chat_openai, 
               model = "gpt-4.1")

generate_story(user_prompt, 
               chat_fn = ellmer::chat_anthropic, 
               model = "claude-sonnet-4-20250514")

Currently, Cloudflare models cannot be used to generate story text as they do not support JSON schema that client$chat_structured uses in generate_story().

Generate prompts for image generation

You may want to generate image illustrations that accompany each paragraph of the generated story. However, providing the story text to an image generation model may not create consistent characters in each scene. So, you’d want to provide detailed prompts for drawing each image. storytelling provides the function generate_image_prompts() to generate detailed prompts for drawing images. Like generate_story(), it also uses Google Gemini but can be changed:

# Now create prompts for image description:
image_prompts <- generate_image_prompts(story$story)
# [1] "Ignis, a young dragon with crimson scales, sharp horns, piercing yellow eyes, and a long, powerful tail, is surrounded by a large hoard of gems and gold coins in his cave, looking wistful and yearning for adventure, with a vast, endless sky visible through the cave entrance."                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
# [2] "Ignis, now in the forgotten Sunken Temple with crumbling stone walls and ancient carvings, holding a scroll with both claws. Around him are moonpetal dew in a vial, a single phoenix feather glowing with embers, and a rare crystal pulsating with geothermal energy. His scales are shimmering, with bones contorting, and fire erupting around him, transforming into a bewildered young man with fiery red hair, fair skin, and a slender build, in a dramatic, arcane setting under the crimson glow of a blood moon."                                                                                                                                                                                                                                                                                                                                                  
# [3] "Ian, a bewildered young man with fiery red hair, fair skin, and a slender build, wearing simple, ill-fitting clothes, is stumbling into a bustling village with cobblestone streets and thatched-roof houses, looking overwhelmed by the weight of the clothes, the strange taste of bread, and the cacophony of human voices. Elara, a traveling merchant with kind eyes, braided brown hair, and wearing practical leather armor, is offering him work and shelter with a warm, welcoming smile."                                                                                                                                                                                                                                                                                                                                                                           
# [4] "Ian, a young man with fiery red hair, fair skin, and a slender build, wearing simple traveling clothes, is looking conflicted and distressed in the Shadowfen, a treacherous swamp with murky water, gnarled trees, and hanging moss. Elara, a woman with braided brown hair and wearing practical leather armor, is pressing him for answers with a concerned expression, her trust unwavering. In the background, Malkor, a ruthless sorcerer with a gaunt face, dark robes, and glowing red eyes, is sensing the dark magic emanating from the Dragon's Tooth, a relic of immense power, with a sinister smile."                                                                                                                                                                                                                                                           
# [5] "Ian, with fiery red hair, fair skin, and a slender build, now partially transformed, with crimson scales appearing on his arms and face and his eyes glowing with dragon fire, wearing torn traveling clothes, is confronting Malkor, the ruthless sorcerer, in the Shadowfen. Malkor, with a gaunt face, dark robes, and glowing red eyes, is wielding dark magic, looking furious. Elara, with braided brown hair and wearing practical leather armor, is standing beside Ian, ready to fight. Ian is shattering the Dragon's Tooth with a powerful blast of fire, severing Malkor's connection to its power, and banishing the sorcerer. Afterwards, Ian, still in his human form but with a knowing look in his eyes, is continuing to travel with Elara, now with a strong bond between them, in a serene, sunlit landscape, symbolizing their journey into the unknown."

Generate images

Now you can provide the image prompts to generate_images() that would return a list with the same length as image_prompts. Each element in this list is an image in the raw format:

all_images <- generate_images(image_prompts, style = "ethereal_fantasy")
length(all_images)
# [1] 5

Note that generate_images() uses the Stable Diffusion XL Base 1.0 model via the Cloudflare API. At the time of writing, this model is in beta which means it is free.

Render images in different styles

You may use different styles for images. For example:

generate_images(image_prompts, style = "lego_movie")
generate_images(image_prompts, style = "impressionist")
generate_images(image_prompts, style = "pokemon")

See the documentation for more styles.

Generate a slide deck with story and illustrations

Now you’re ready to combine everything in a nice looking slide deck:

# Copy a QMD template file to a desired directory:
path <- copy_template("C:/Users/Your/Path")

# Combine text and images in a revealjs slide deck:
create_slides(
  input_qmd = path,
  theme = "beige",
  title = story$title,
  story = story$story,
  images = all_images,
  audios = FALSE
)

Add audio narration in your slide deck

You can also create slides with audio playback on each slide. Here, we use a different template that can incorporate audio input:

# Generate audio from story text:
audios <- generate_audio(story)

path2 <- copy_template("C:/Users/Your/Path", audio = TRUE)

create_slides(
  input_qmd = path2,
  theme = "sky",
  title = story$title,
  story = story$story,
  images = all_images,
  audios = audios
)

Note that generate_audio() uses the melotts text-to-speech model by default (via the Cloudflare API). This model costs you “neurons” (Cloudflare’s terminology for cost) but a generous amount can be used for free. You may also use Google text to speech (TTS) model. You’d need to have ffmpeg installed for converting the Google TTS response (pcm) to mp3. generate_audio() takes care of this conversion

About

No description, website, or topics provided.

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE
MIT
LICENSE.md

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages