一、前言
平时我们想用AI写点小故事、随笔、文案,要么需要联网、要么要充会员、要么担心内容上传泄露隐私。
很多小伙伴电脑配置一般,跑不动大型AI项目,想找一个简单、干净、低配也能跑、断网也能用的AI文本生成工具,真的很难。
今天给大家分享我写的这款 Python + Streamlit AI叙事生成工具,主打一个:免费、本地、干净、无脑运行。
一共三种使用模式:
1.完全离线(没网也能跑,不用任何AI模型)
2. Ollama 本地大模型(纯本地推理,不上传数据)
3. 云端 API 接口(追求高质量生成)
不管你是学生练手、做课程设计、平时写文案、写小故事,这个项目都够用,界面好看、功能齐全、代码无报错。
二、项目能干啥?(真实实用功能)
不搞虚的,直接说能用的功能:
- 四种写作风格:都市日常、古风、悬疑、治愈温情,日常写文案完全够用
- 一键随机生成三套素材:人物、场景、剧情冲突自动搭配,不用自己苦想
- 自定义角色和世界观:想写校园、科幻、末世都可以自己改
- 可调剧情长短、起伏程度:想写短文、长文、平淡日常、跌宕剧情随便拖滑块
- 支持续写:上次写一半,这次可以接着往下写
- 历史记录保存复用:喜欢的写作风格可以一键重来
- 一键导出TXT/MD:写完直接保存,方便复制到公众号、文档、作业
- 双主题界面:深色炫酷、浅色干净,看你喜好切换
三、环境搭建(超简单)
- 安装依赖
新建 requirements.txt,写入下面两行:
streamlit==1.35.0
requests
终端输入安装命令:
pip install -r requirements.txt - 运行项目
把代码保存为 main.py,直接运行:
streamlit run main.py
自动弹出网页界面,不用前端、不用配置,开箱即用。
四、完整可运行源码(已修复全部BUG)
全网最稳版本,修复了 Ollama 接口报错、URL 解析异常等常见问题,直接复制即可运行。
源码:
-- coding: utf-8 --
接地气 AI 故事生成工具|离线 + 本地大模型 + 云端三模式
低配电脑可跑、断网可用,适合练手/课程设计/文案创作
import streamlit as st
import random
import time
import requests
import datetime
页面全局配置
st.set_page_config(
page_title=“AI故事生成工具”,
page_icon=“🧠”,
layout=“wide”,
initial_sidebar_state=“expanded”
)
#初始化缓存(保存历史、自定义角色))
if “feature_cache” not in st.session_state:
st.session_state.feature_cache = {}
if “history_list” not in st.session_state:
st.session_state.history_list = []
if “custom_char_lib” not in st.session_state:
st.session_state.custom_char_lib = []
if “last_content” not in st.session_state:
st.session_state.last_content = “”
界面主题切换
def set_theme(mode):
if mode == “dark”:
css = “”"
“”"
st.markdown(css, unsafe_allow_html=True)
侧边栏主题
theme_opt = st.sidebar.radio(界面主题题", [“深色科技模式”, 浅色简约模式式"])
set_theme(“dark” if theme_opt == “深色科技模式” else “light”)
st.sidebar.divider()
三种推理模式切换
run_mode = st.sidebar.radio(“运行模式”, [
“纯离线使用(无需网络)”,
“Ollama本地大模型(不上网)”,
“云端API生成(高质量)”
])
api_key = “”
api_url = “”
ollama_host = “http://127.0.0.1:11434/api/generate”
ollama_model = “qwen:7b”
if run_mode == “云端API生成(高质量)”:
api_key = st.sidebar.text_input(“API Key”, type=“password”)
api_url = st.sidebar.text_input(“接口地址”)
elif run_mode == “Ollama本地大模型(不上网)”:
ollama_host = st.sidebar.text_input(“Ollama地址”, value=ollama_host)
ollama_model = st.sidebar.text_input(“模型名称”, value=ollama_model)
历史记录复用
st.sidebar.divider()
st.sidebar.markdown(### 📜 历史记录记录"if st.session_state.history_list:t for idx, item in enumerate(st.session_state.history_list[-5:])😃 if st.sidebar.button(f"复用记录 {idx+1}“, key=f"his_{idx}”)😃 st.session_state.feature_cache = item[“feat”]"]
else:
st.sidebar.caption(“暂无记录”)
四大写作素材库
style_material = {
“都市日常”: {
“char”: [“内敛沉稳的老手艺人”,“不甘平庸的打工人”,“外冷内热的小店店主”,“不善言辞的程序员”],
“scene”: [“深夜便利店”,“高层阳台”,“老街书店”,“夜市小摊”],
“conflict”: [“老店即将拆迁”,“捡到匿名旧信”,“反复偶遇陌生人”,“旧信物勾起往事”]
},
“古风叙事”: {
“char”: [“隐世郎中”,“落魄书生”,“镖局走卒”,“茶馆掌柜”],
“scene”: [“烟雨古巷”,“山间茶寮”,“渡口木船”,“破败书院”],
“conflict”: [“遗失祖传玉佩”,“故人十年之约”,“江湖旧仇浮现”,“一纸离别书信”]
},
“悬疑故事”: {
“char”: [“档案管理员”,“私家寻访人”,“旧物收藏家”,“夜班守馆人”],
“scene”: [“废弃码头”,“照相馆暗房”,“尘封储藏室”,“雾中小镇”],
“conflict”: [“长辈隐藏秘密”,“无主遗物线索”,“诡异重复梦境”,“一模一样的信物”]
},
“治愈温情”: {
“char”: [“旅行摄影师”,“图书馆管理员”,“返乡青年”,“糕点师傅”],
“scene”: [“山间村落”,“绿皮火车”,“雪天酒馆”,“午后花店”],
“conflict”: [“迟来多年留言”,“一场大雨邂逅”,“丢失的童年物件”,“萍水相逢的善意”]
}
emotion_list = [“温暖治愈、结局美好”,“留白开放式、引人遐想”,“现实写实、略带遗憾”,“平淡日常、温柔治愈”]"]
离线文字微调微def weight_adjust(text)😃:
modifiers = [“细微的”, “悄然的”, “静默的”, “温柔的”, “深沉的”, “淡淡的”]"]
return random.choice(modifiers) + textxt
离线生成核心(不用任何模型、断网也能用)
def offline_generate(char, scene, conflict, emotion, world, length, drama, gen_type):
scene = weight_fluctuate(scene)
conflict = weight_fluctuate(conflict)
len_tip = “长篇详细描写” if length > 0.7 else “简短精炼内容”
drama_tip = “剧情起伏大、冲突明显” if drama > 0.6 else “日常平缓叙事”
base_head = f"""【离线生成结果】世界观:{world}
人物:{char}
场景:{scene}
核心剧情:{conflict}
整体风格:{emotion}
篇幅:{len_tip}
剧情强度:{drama_tip}
“”"
if gen_type == “短篇文艺随笔”:
body = f"““在{scene}平平淡淡的日子里,{char}早已习惯了一成不变的生原本安稳平静的日常,被{conflict}悄然打破。破。没有轰轰烈烈的剧情,只有普通人藏在心底的细碎情绪,整体风格{emotion}。””"
elif gen_type == “完整故事大纲”:
body = f"““1.铺垫:描绘{scene}的日常氛围,交代主角的生活状态;
2.转折:{conflict}突然出现,打破平静生活;
3.发展:主角顺着线索慢慢探索,发现背后的故事;
4.结尾:以{emotion}的风格收束全文,故事完整闭环。””"
else:
body = f"暮色慢慢铺满{scene},{char}本以为又是普通平淡的一天,直到{conflict}悄然出现。过往的回忆悄然翻涌,平凡的日常多了一丝波澜,整篇故事质感{emotion}。}。“”"
full_text = base_head + body if length > 0.6: full_text += "\n\n生活大多是平淡的,那些细碎的相遇、遗憾与温柔,拼凑成了普通人最真实的人生。" return full_textOllama本地大模型调用
def ollama_infer(char, scene, conflict, emotion, world, gen_type, model, host):
prompt = f"写一段{gen_type},世界观:{world},主角:{char},场景:{scene},剧情冲突:{conflict},整体风格:{emotion文笔自然流畅,直接输出正文。文。"
payload = {“model”: model, “prompt”: prompt, “stream”: False}
try:
res = requests.post(host, json=payload, timeout=30)
return res.json()[“response”] if res.status_code == 200 else “Ollama请求出错”
except Exception as e:
return f"本地模型连接失败,请确认Ollama已启动:{str(e)}"
云端API调用
def cloud_api_infer(char, scene, conflict, emotion, world, gen_type, key, url):
prompt = f"生成一段{gen_type},世界观{world},人物{char},场景{scene},剧情冲突{conflict},风格{emotion输出高质量正文。文。"
headers = {“Authorization”: f"Bearer {key}“, “Content-Type”: “application/json”}
data = {“model”: “general”,“messages”: [{“role”:“user”,“content”: prompt}],“temperature”:0.7}
try:
resp = requests.post(url, headers=headers, json=data, timeout=25)
return resp.json().get(“output”,{}).get(“text”,“接口返回数据异常”)
except Exception as e:
return 接口请求失败:败:{str(e)}”
页面主体
st.title(“🧠 免费AI故事生成器|离线可用+本地大模型”)
mode_tip = {
“纯离线使用(无需网络)”: 零网络、零模型,低配电脑随便跑随便跑",
“Ollama本地大模型(不上网)”: “✅ 本地AI推理,数据绝不外泄”,
“云端API生成(高质量)”: “✅ 全网高质量文案生成”
}
st.subheader(mode_tip[run_mode])
st.divider()
参数设置区域
col_a, col_b = st.columns([1,1])
with col_a:
st.markdown(“### 🔧 创作参数设置”)
style_sel = st.selectbox(“写作风格”, list(style_material.keys()))
custom_char = st.text_inp自定义主角(留空则自动随机)动随机)“)
custom_world = st.text_inp自定义世界观(留空则默认为日常)认日常)”)
gen_type = st.selectbox(“生成类型”, [“短篇文艺随笔”, “完整故事大纲”, “小说开篇正文”])
text_len = st.slid文章篇幅(比例)文章篇幅", 0.1, 0.9, 0.4, 0.1)
drama_level = st.slid剧情起伏程度(比例)起伏程度", 0.1, 0.9, 0.3, 0.1)
with col_b:
st.markdown(“### 📊 一键生成剧情素材”)
refresh_3 = st.button(“🎲 随机生成3套剧情方案”, use_container_width=True)
add_custom_char = st.text_input(“添加自定义人物素材”)
if add_custom_char and st.button(“保存人物”):
st.session_state.custom_char_lib.append(add_custom_char)
st.suc保存成功,下次可直接使用!可以直接用!")
st.divider()
mat_pool = style_material[style_sel] if refresh_3 or not st.session_state.feature_cache: group_list = [] for _ in range(3): ch = random.choice(mat_pool["char"] + st.session_state.custom_char_lib) if st.session_state.custom_char_lib else random.choice(mat_pool["char"]) group_list.append({ "char": ch, "scene": random.choice(mat_pool["scene"]), "conflict": random.choice(mat_pool["conflict"]), "emotion": random.choice(emotion_list) }) st.session_state.feature_cache = group_list for idx, g in enumerate(st.session_state.feature_cache): st.markdown(f"**方案{idx+1}**") st.write(f"人物:{g['char']}|场景:{g['scene']}") st.write(f"剧情:{g['conflict']}|风格:{g['emotion']}") if st.button(f"选用方案{idx+1}", key=f"use_{idx}"): st.session_state.active_feat = gst.divider()
continue_write = st.checkbox(“开启上下文续写(接着上次内容写)”)
run_btn = st.button(“🚀 一键生成AI文案”, type=“primary”, use_container_width=True)
output_text = “”
if run_btn:
if “active_feat” not in st.session_state:
st.warning(“请先点击选用一套剧情方案!”)
else:
feat = st.session_state.active_feat
bar = st.progress(0)
for pct in range(0, 101, 10):
bar.progress(pct)
time.sleep(0.12)
bar.empty()
final_char = custom_char.strip() if custom_char.strip() else feat["char"] final_world = custom_world.strip() if custom_world.strip() else "普通现实生活世界观" prefix = f"接续上文创作:{st.session_state.last_content}\n" if (continue_write and st.session_state.last_content) else "" if run_mode == "纯离线使用(无需网络)": res_raw = offline_generate(final_char, feat["scene"], feat["conflict"], feat["emotion"], final_world, text_len, drama_level, gen_type) elif run_mode == "Ollama本地大模型(不上网)": res_raw = ollama_infer(final_char, feat["scene"], feat["conflict"], feat["emotion"], final_world, gen_type, ollama_model, ollama_host) else: res_raw = cloud_api_infer(final_char, feat["scene"], feat["conflict"], feat["emotion"], final_world, gen_type, api_key, api_url) output_text = prefix + res_raw st.session_state.last_content = output_text st.session_state.history_list.append({"feat": feat, "content": output_text})结果展示与导出
st.divider()
st.markdown(“### 📄 生成结果”)
st.text_area(“文案内容”, output_text, height=420)
st.caption(f"当前总字数:{len(output_text)}")
if output_text:
ts = datetime.datetime.now().strftime(“%Y%m%d_%H%M%S”)
c1, c2 = st.columns(2)
with c1:
st.download_button(“📥 导出 TXT 文件”, output_text, f"AI文案_{ts}.txt")
with c2:
st.download_button(“📥 导出 MD 文件”, f"# AI故事生成结果\n{output_text}“, f"AI文案_{ts}.md”)
st.divider()
st.caption("纯学习开源## 五、项目总结(人话总结)项这个项目最大的优点就是不折腾、不氪金、不联网也能用。氪不用部署复杂大模型、不用调参、不用花钱开会员,低配电脑、学生机都能流畅运行。、平时写随笔、写小故事、凑文案、做课程设计、练习 Python Web 开发,都完全够用。 代码干净无冗余、已知 BUG 已修复,直接复制运行即可,非常适合新手入门练手。常适合新手入门练手。