全部文章

随机生成图像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
    )
image
 
 
 
 
 
posted @ 2025-08-26 22:51  指尖下的世界  阅读(193)  评论(0)    收藏  举报