随机生成图像picsum.photos
https://picsum.photos/
1. Picsum Photos 的核心特点
- 无版权限制:所有图片均由摄影师自愿贡献,可免费用于商业 / 非商业场景(无需注明来源,具体可参考 官方协议);
- 灵活的 URL 规则:通过调整 URL 中的参数,可以精准控制图片的「ID、尺寸、风格」等,满足不同需求;
- 稳定可靠:作为行业常用的占位图服务,无需担心链接失效或访问问题(国内访问也较稳定)。
2. 你代码中 URL 的参数解析
以
https://picsum.photos/id/237/500/300
为例,各部分含义如下:
URL 片段 | 作用说明 |
---|---|
https://picsum.photos |
服务根域名,固定不变; |
id/237 |
指定图片的唯一 ID(237 是具体 ID),同一 ID 对应固定图片(比如 id/237 固定是一只小狗的图); |
500/300 |
指定图片的尺寸(宽 500px × 高 300px),顺序为「宽在前、高在后」; |
3. 如何自定义 Picsum 图片?
如果需要替换示例中的图片,只需按照规则修改 URL 即可:
- 随机图片:去掉
id/xxx
,直接用https://picsum.photos/500/300
(每次访问会返回不同图片); - 指定风格 / 场景:通过 ID 间接控制(比如
id/1039
是山脉、id/1062
是暴雨,可在 Picsum 官网 浏览所有带 ID 的图片); - 调整尺寸:直接修改末尾的数字,比如
https://picsum.photos/id/237/800/600
(宽 800px × 高 600px)。
4. 为什么在 Gradio 示例中常用它?
你代码中的
gr.Examples
是 Gradio(快速构建 Web 交互界面的库)的「示例案例组件」,用于给用户展示「输入参数对应的效果」。使用 Picsum 图片的核心原因是:
- 无需手动准备图片资源:避免开发者为了演示而单独拍摄 / 下载图片,节省时间;
- 保证示例稳定性:所有示例图片链接永久有效,用户打开界面时能直接加载,不会出现「图片失效」的问题;
- 贴合场景需求:Picsum 提供自然风景、动物、人物、建筑等多种类型图片,能匹配「根据图片生成故事 / 音频」这类场景的示例需求(比如小狗图对应「温馨故事」、山脉图对应「冒险故事」)。
如果需要替换示例图片,可直接在 Picsum 官网 搜索需要的图片(比如搜索「cat」「ocean」),复制对应的带 ID 链接,替换代码中的 URL 即可。
代码中的应用:
import gradio as gr
from transformers import BlipProcessor, BlipForConditionalGeneration, AutoTokenizer, AutoModelForCausalLM, pipeline
import torch
from gtts import gTTS
import os
import uuid # 生成唯一音频文件名,防止覆盖
from datetime import datetime # 辅助清理旧文件
# 确保中文显示正常
os.environ["PYTHONUTF8"] = "1"
# ---------------------- 关键:创建音频持久化存储目录 ----------------------
AUDIO_SAVE_DIR = "./generated_audio_files"
os.makedirs(AUDIO_SAVE_DIR, exist_ok=True) # 目录不存在则自动创建
# ---------------------- 模型加载 ----------------------
# 图像描述模型(BLIP)
processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
# 适配 CPU/GPU 环境的 dtype(避免内存报错)
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
model = BlipForConditionalGeneration.from_pretrained(
"Salesforce/blip-image-captioning-base",
torch_dtype=dtype
)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
# 故事生成模型(Qwen2-0.5B-Instruct,CPU 可稳定运行)
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-0.5B-Instruct", trust_remote_code=True)
story_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2-0.5B-Instruct",
trust_remote_code=True,
torch_dtype=dtype # 减少 CPU 内存占用
).to(device)
# 确保 tokenizer 有 pad_token(避免生成时报错)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# ---------------------- 核心功能函数 ----------------------
def generate_caption(image):
"""根据图片生成中文描述"""
if image is None:
return "请上传一张图片"
inputs = processor(image, return_tensors="pt").to(device, dtype=dtype)
out = model.generate(**inputs, max_length=50, num_beams=3) # Beam Search 提升描述质量
caption = processor.decode(out[0], skip_special_tokens=True)
# 确保描述为中文(BLIP 基础版可能输出英文)
if not any('\u4e00' <= c <= '\u9fff' for c in caption):
try:
translator = pipeline("translation", model="Helsinki-NLP/opus-mt-en-zh")
caption = translator(caption, max_length=50)[0]['translation_text']
except Exception as e:
print(f"翻译失败: {e}")
caption = "图片内容无法识别,请尝试另一张图片"
return caption
def generate_story(caption, max_length, style, creativity):
"""根据描述生成指定风格的中文故事"""
invalid_captions = ["请上传一张图片", "图片内容无法识别,请尝试另一张图片"]
if not caption or caption in invalid_captions:
return "请先上传有效图片生成描述"
# 创意值映射为 temperature(0→保守,10→奔放)
temperature = max(0.3, min(2.0, 0.3 + (creativity / 10) * 1.7))
# 优化提示词(适配 Qwen 模型,避免跑题)
prompt = f"""任务:根据图片描述写{style}风格故事
要求:1. 纯中文;2. 字数控制在{max_length}字左右;3. 符合风格特点,语句通顺
图片描述:{caption}
故事:"""
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512).to(device)
outputs = story_model.generate(
**inputs,
max_length=len(inputs["input_ids"][0]) + max_length,
temperature=temperature,
do_sample=True,
top_p=0.9, # 平衡创意与逻辑
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id
)
story = tokenizer.decode(outputs[0], skip_special_tokens=True).replace(prompt, "").strip()
# 截断过长故事并补充结尾
if len(story) > max_length:
story = story[:max_length].rsplit('。', 1)[0] + "。(故事已截断以符合字数要求)"
return story
def text_to_speech(text):
"""将故事文本转为 MP3 音频(持久化存储,支持试听/下载)"""
if not text or text == "请先上传有效图片生成描述":
return None
try:
# 生成唯一文件名(避免多轮生成覆盖,格式:时间+随机ID.mp3)
unique_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
audio_path = os.path.join(AUDIO_SAVE_DIR, f"story_audio_{unique_id}.mp3")
# 生成中文音频(slow=False 语速正常,提升听感)
tts = gTTS(text=text, lang='zh-CN', slow=False)
tts.save(audio_path)
# 清理3天前的旧音频(避免目录占用过大)
clean_old_audio(days=3)
return audio_path # 返回文件路径,Gradio 自动识别并渲染控件
except Exception as e:
print(f"音频生成失败: {str(e)}")
return None
def clean_old_audio(days=3):
"""清理指定天数前的旧音频文件"""
now = datetime.now()
for filename in os.listdir(AUDIO_SAVE_DIR):
if filename.endswith(".mp3"):
file_path = os.path.join(AUDIO_SAVE_DIR, filename)
# 获取文件修改时间
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
# 超过指定天数则删除
if (now - file_mtime).days >= days:
os.remove(file_path)
print(f"已清理旧音频:{filename}")
def process_image(image, max_length, style, creativity):
"""端到端处理:图片→描述→故事→音频"""
try:
caption = generate_caption(image)
story = generate_story(caption, max_length, style, creativity)
audio = text_to_speech(story)
return caption, story, audio
except Exception as e:
error_msg = f"处理出错: {str(e)[:100]}" # 限制错误信息长度,避免界面混乱
return error_msg, "", None
# ---------------------- Gradio 界面(重点配置音频控件) ----------------------
with gr.Blocks(title="图片转故事与音频", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🌟 图片转故事与音频生成器")
gr.Markdown("上传图片 → 生成中文描述 → 定制风格故事 → 试听/下载音频")
with gr.Row():
# 左侧:输入与参数配置区
with gr.Column(scale=1, min_width=300):
image_input = gr.Image(
type="pil",
label="上传图片",
height=280, # 增大图片上传区域,提升体验
show_label=True,
interactive=True
)
# 参数配置折叠面板(更简洁)
with gr.Accordion("📝 故事参数设置", open=True):
max_length = gr.Slider(
minimum=50, # 最低字数调整为50,避免生成过短内容
maximum=1000,
value=300,
step=50,
label="故事最大字数",
info="建议 200-500 字(生成更快)"
)
style = gr.Dropdown(
choices=["搞笑", "武侠", "爱情", "动作", "科幻", "悬疑", "奇幻", "温馨", "恐怖", "冒险", "童话"],
value="温馨",
label="故事风格",
interactive=True
)
creativity = gr.Slider(
minimum=0,
maximum=10,
value=5,
step=1,
label="创意值(0-10)",
info="0=保守常规 | 10=脑洞大开"
)
generate_btn = gr.Button(
"🚀 生成描述+故事+音频",
variant="primary",
size="lg" # 增大按钮,提升辨识度
)
# 右侧:输出区(重点配置音频控件)
with gr.Column(scale=2, min_width=500):
caption_output = gr.Textbox(
label="🖼️ 图片描述",
lines=2,
interactive=False,
show_copy_button=True # 增加复制按钮,方便用户使用
)
story_output = gr.Textbox(
label="📖 生成的故事",
lines=12,
interactive=False,
show_copy_button=True
)
# 关键:配置音频控件支持试听和下载
audio_output = gr.Audio(
label="🎵 故事音频(可试听/下载)",
type="filepath", # 必须设为 filepath,Gradio 才能识别文件并渲染控件
interactive=True,
show_download_button=True, # 显式开启下载按钮
autoplay=False # 关闭自动播放,避免打扰用户
)
# 绑定生成按钮事件(显示进度条,提升体验)
generate_btn.click(
fn=process_image,
inputs=[image_input, max_length, style, creativity],
outputs=[caption_output, story_output, audio_output],
show_progress="full", # 显示完整进度条,告知用户处理状态
queue=True # 开启任务队列,支持多轮生成不卡顿
)
# 示例数据(帮助用户快速测试功能)
gr.Examples(
examples=[
["https://picsum.photos/id/237/500/300", 300, "温馨", 5], # 小狗→温馨故事
["https://picsum.photos/id/1039/500/300", 400, "冒险", 8], # 山脉→冒险故事
["https://picsum.photos/id/1062/500/300", 200, "搞笑", 7] # 暴雨→搞笑故事
],
inputs=[image_input, max_length, style, creativity],
outputs=[caption_output, story_output, audio_output],
fn=process_image,
label="💡 点击示例快速测试"
)
# ---------------------- 启动配置 ----------------------
if __name__ == "__main__":
# 本地运行:自动打开浏览器,端口 7860
demo.launch(
debug=False,
share=False, # 本地运行无需公网链接,部署到 Hugging Face 时会自动忽略
server_name="0.0.0.0",
server_port=7860
)
