python实现免费ai短剧无限分集管理系统代码BfwStoryForge

代码语言:python

所属分类:其他

版本4(最新):python实现免费ai短剧无限分集管理系统代码BfwStoryForge

版本3(旧版):python实现ai短剧漫剧生成管理seedance免费提示词版本BfwStoryForge代码

版本2(旧版):python+agnesai的api实现免费ai短剧生成系统bfwStoryForge代码

版本1(旧版):python+阿里大模型api实现模拟小云雀ai短剧生成工具BfwStoryForge代码

代码描述:python实现免费ai短剧无限分集管理系统代码BfwStoryForge ,将剧本一集集的生成seedance2专用视频提示词,并自动整理相关资产图片和声音,支持声音文字描述,无需api,使用常用的ai聊天大模型既可生成剧本分镜提示词,支持10-30秒分镜片段设置,是新手免费入门ai短剧的好工具。

代码标签: python 免费 ai 短剧 无限 分集 管理 系统 代码

下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AI有声视频无限分集短剧生成工作流系统 - 纯手动离线免费版 · AIF
无 shots 模式:分段镜头节奏由 seedance2_prompt 时间戳自由划分;
资产库支持变种(含AI生图提示词一键复制);分段出镜资产可选;@图片N/@音频N 按 asset_refs 顺序编号;
自动注入全局风格与画面比例;多人物分镜强制描述站位。
连续片段管理支持拖拽排序、卡片侧边悬停快捷插入新卡片。
"""

import os
import re
import json
import ast
import uuid
import base64
import time
import requests
import threading
from datetime import datetime
from pathlib import Path
from flask import Flask, request, jsonify, send_from_directory

# ============================================================
# 配置与初始化
# ============================================================
WORK_DIR = Path("workspace")
WORK_DIR.mkdir(exist_ok=True)
PROJECTS_FILE = WORK_DIR / "projects.json"

app = Flask(__name__)

KIND_KEY = {"char": "characters", "prop": "props", "scene": "scenes"}

# ================= 阿里云 DashScope API 配置 =================
API_KEY = os.getenv("DASHSCOPE_API_KEY", "")
WORKSPACE_ID = os.getenv("WORKSPACE_ID", "llm-") 

# ============================================================
# 辅助工具
# ============================================================
def file_to_base64(filepath):
    if not filepath or not os.path.exists(filepath):
        return None
    try:
        with open(filepath, "rb") as f:
            return base64.b64encode(f.read()).decode('utf-8')
    except Exception as e:
        print(f"Error reading file {filepath}: {e}")
        return None

def _aif_scalar(value):
    value = value.strip()
    if not value:
        return ""
    if value.startswith("`") and value.endswith("`") and len(value) >= 2:
        value = value[1:-1].strip()
    if value in ("null", "NULL", "None", "~"):
        return None
    if value.lower() in ("true", "false"):
        return value.lower() == "true"
    if (len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"')):
        try:
            return ast.literal_eval(value)
        except Exception:
            return value[1:-1]
    if value.startswith("[") and value.endswith("]"):
        inner = value[1:-1].strip()
        if not inner:
            return []
        parts, buf, quote, depth = [], [], None, 0
        for ch in inner:
            if quote:
                buf.append(ch)
                if ch == quote and (len(buf) < 2 or buf[-2] != "\\"):
                    quote = None
            elif ch in ("'", '"'):
                quote = ch
                buf.append(ch)
            elif ch in "[{":
                depth += 1; buf.append(ch)
            elif ch in "]}":
                depth -= 1; buf.append(ch)
            elif ch == "," and depth == 0:
                parts.append("".join(buf).strip()); buf = []
            else:
                buf.append(ch)
        if buf:
            parts.append("".join(buf).strip())
        return [_aif_scalar(x) for x in parts if x != ""]
    try:
        if re.fullmatch(r"-?\d+", value):
            return int(value)
        if re.fullmatch(r"-?(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?", value):
            return float(value)
    except Exception:
        pass
    return value


def _aif_clean_lines(text):
    text = text.replace("\r\n", "\n").replace("\r", "\n").strip()
    text = re.sub(r"^\s*```(?:aif|AIF|ai-flow|text)?\s*\n?", "", text)
    text = re.sub(r"\n?\s*```\s*$", "", text)
    lines = text.split("\n")
    header_idx = None
    for i, line in enumerate(lines):
        if line.strip().lower() in ("aif", "aif 1", "aif/1", "aif 1.0", "aif/1.0"):
            header_idx = i
            break
    if header_idx is not None:
        lines = lines[header_idx + 1:]
    else:
        first = None
        for i, line in enumerate(lines):
            s = line.strip()
            if re.match(r"^[A-Za-z_][\w-]*\s*:", s) or re.match(r"^[A-Za-z_][\w-]*\s*$", s):
                first = i
                break
        if first is not None:
            lines = lines[first:]
    return lines


def parse_aif(text):
    lines = _aif_clean_lines(text)
    while lines and not lines[0].strip():
        lines.pop(0)
    if not lines:
        raise ValueError("AIF 内容为空。")

    rows = []
    for raw in lines:
        if not raw.strip() or raw.lstrip().startswith("#"):
            continue
        indent = len(raw) - len(raw.lstrip(" "))
        rows.append((indent, raw.strip()))

    def split_key(s):
        depth, quote = 0, None
        for i, ch in enumerate(s):
            if quote:
                if ch == quote and (i == 0 or s[i-1] != "\\"):
                    quote = None
            elif ch in ("'", '"'):
                quote = ch
            elif ch in "[{(":
                depth += 1
            elif ch in "]})":
                depth -= 1
            elif ch == ":" and depth == 0:
                key = s[:i].strip()
                val = s[i+1:].strip()
                if key:
                    return key, val
        return None, None

    def parse_block(pos, indent):
        if pos >= len(rows) or rows[pos][0] < indent:
            return {}, pos
        is_list = rows[pos][0] == indent and rows[pos][1].startswith("-")
        obj = [] if is_list else {}

        while pos < len(rows):
            ind, content = rows[pos]
            if ind < indent:
                break
            if ind > indent:
                break

            if is_list:
                if not content.startswith("-"):
                    break
                item = content[1:].strip()
                if not item:
                    if pos + 1 < len(rows) and rows[pos+1][0] > indent:
                        child, pos = parse_block(pos+1, rows[pos+1][0])
                        obj.append(child)
                    else:
                        obj.append(None); pos += 1
                    continue
                k, v = split_key(item)
                if k is not None:
                    d = {}
                    if v == "":
                        if pos + 1 < len(rows) and rows[pos+1][0] > indent:
                            child, nextpos = parse_block(pos+1, rows[pos+1][0])
                            d[k] = child; pos = nextpos
                        else:
                            d[k] = {}; pos += 1
                    elif v == "|":
                        vals=[]; pos += 1
                        while pos < len(rows) and rows[pos][0] > indent:
                            vals.append(rows[pos][1]); pos += 1
                        d[k] = "\n".join(vals)
                    else:
                        d[k] = _aif_scalar(v); pos += 1
                    if pos < len(rows) and rows[pos][0] > indent:
                        child, nextpos = parse_block(pos, rows[pos][0])
                        if isinstance(child, dict):
                            d.update(child)
                            pos = nextpos
                    obj.append(d)
                else:
                    obj.append(_aif_scalar(item)); pos += 1
            else:
                k, v = split_key(content)
                if k is None:
                    k = content.strip()
                    v = ""
                if v == "":
                    if pos + 1 < len(rows) and rows[pos+1][0] > indent:
                        child, pos = parse_block(pos+1, rows[pos+1][0])
                        obj[k] = child
                    else:
                        obj[k] = {}; pos += 1
                elif v == "|":
                    vals=[]; pos += 1
                    while pos < len(rows) and rows[pos][0] > indent:
                        vals.append(rows[pos][1]); pos += 1
                    obj[k] = "\n".join(vals)
                else:
                    obj[k] = _aif_scalar(v); pos += 1

        return obj, pos

    root, pos = parse_block(0, rows[0][0])
    if not isinstance(root, dict):
        raise ValueError("AIF 根节点必须是对象。")
    return root


def extract_aif_from_text(text):
    if not isinstance(text, str) or not text.strip():
        raise ValueError("没有提供 AIF 文本。")
    try:
        return parse_aif(text)
    except Exception as first_error:
        m = re.search(r'(?m)^[ \t]*(?:AIF(?:/1(?:\.0)?)?|title\s*:|script\s*$)', text)
        if m:
            try:
                return parse_aif(text[m.start():])
            except Exception:
                pass
        raise ValueError(f"无法解析 AIF:{first_error}")


def _aif_quote(value):
    if value is None:
        return "null"
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, (int, float)):
        return str(value)
    s = str(value)
    if (not s or s.strip() != s or "\n" in s or ":" in s or s.startswith(("[", "{", "#", "-", "`"))):
        return json.dumps(s, ensure_ascii=False)
    return s


def dump_aif(data, indent=0):
    out = ["AIF 1"]

    def emit_value(key, value, level):
        pad = " " * level
        if isinstance(value, dict):
            out.append(f"{pad}{key}:")
            for k, v in value.items():
                emit_value(k, v, level + 2)
        elif isinstance(value, list):
            if not value:
                out.append(f"{pad}{key}: []")
            else:
                out.append(f"{pad}{key}:")
                for item in value:
                    if isinstance(item, dict):
                        first = True
                        for k, v in item.items():
                            if first:
                                if isinstance(v, (dict, list)):
                                    out.append(" " * (level + 2) + f"- {k}:")
                                    if isinstance(v, dict):
                                        for sk, sv in v.items(): emit_value(sk, sv, level + 6)
                                    else:
                                        emit_value("", v, level + 6)
                                else:
                                    out.append(" " * (level + 2) + f"- {k}: {_aif_quote(v)}")
                                first = False
                            else:
                                emit_value(k, v, level + 4)
                    else:
                        out.append(" " * (level + 2) + f"- {_aif_quote(item)}")
        elif isinstance(value, str) and "\n" in value:
            out.append(f"{pad}{key}: |")
            for line in value.splitlines():
                out.append(" " * (level + 2) + line)
        else:
            out.append(f"{pad}{key}: {_aif_quote(value)}")

    if isinstance(data, dict):
        for k, v in data.items():
            emit_value(k, v, 0)
    return "\n".join(out) + "\n"

# ============================================================
# 资产变种 / 出镜资产 / 编号 相关
# ============================================================
def _ensure_group_variants(group, kind):
    if not isinstance(group, dict):
        return group
    group.setdefault("id", f"{kind}_{uuid.uuid4().hex[:6]}")
    group.setdefault("name", "未命名")
    group.setdefault("description", "")
    vs = group.get("variants")
    if not isinstance(vs, list) or not vs:
        v = {"id": group["id"], "label": "默认", "image_url": group.get("image_url")}
        if kind == "char":
            v["voice_url"] = group.get("voice_url")
            v["voice_description"] = group.get("voice_description", "")
            v["sample_dialogue"] = group.get("sample_dialogue", "")
        group["variants"] = [v]
    for v in group["variants"]:
        if not isinstance(v, dict):
            continue
        v.setdefault("id", f"{group['id']}_v{uuid.uuid4().hex[:4]}")
        v.setdefault("label", "默认")
        v.setdefault("image_url", None)
        if kind == "char":
            v.setdefault("voice_url", None)
            v.setdefault("voice_description", "")
            v.setdefault("sample_dialogue", "")
    return group


def _normalize_assets(project):
    for kind in ("char", "prop", "scene"):
        key = KIND_KEY[kind]
        project[key] = [_ensure_group_variants(g, kind) for g in project.get(key, []) if isinstance(g, dict)]


def _find_variant(project, kind, vid):
    for g in project.get(KIND_KEY.get(kind, ""), []):
        for v in g.get("variants", []):
            if str(v.get("id")) == str(vid):
                return g, v
    return None, None


def _resolve_variant_id(project, kind, val):
    g, v = _find_variant(project, kind, val)
    if v:
        return v["id"]
    for g in project.get(KIND_KEY.get(kind, ""), []):
        if str(g.get("name")) == str(val):
            if g.get("variants"):
                return g["variants"][0]["id"]
        for v in g.get("variants", []):
            if str(v.get("label")) == str(val):
                return v["id"]
    return None


def _number_refs(project, refs):
    out, img, aud = [], 0, 0
    for r in refs:
        g, v = _find_variant(project, r.get("type"), r.get("id"))
        if not v:
            continue
        img += 1
        e = {"type": r["type"], "id": v["id"], "name": g.get("name", ""), "label": v.get("label", ""), "img_no": img}
        if r["type"] == "char":
            aud += 1
            e["aud_no"] = aud
        out.append(e)
    return out


def _convert_prompt_markers(prompt, nums):
    if not prompt:
        return prompt
    def repl_img(m):
        inner = m.group(0)[1:-1]
        inner = re.sub(r"^参考", "", inner).strip()
        kind = None
        for prefix, k in (("角色", "char"), ("场景", "scene"), ("道具", "prop")):
            if inner.startswith(prefix):
                kind = k; inner = inner[len(prefix):].strip(); break
        inner = re.sub(r"(图片|视频|音色文件|音色)$", "", inner).strip()
        f.........完整代码请登录后点击上方下载按钮下载查看

网友评论0