gpt-image-2-web 0.6.6

Self-hosted Docker/Web server for GPT Image 2.
#![allow(unused_imports)]

use super::*;

pub(crate) fn metadata_object(job: &Value) -> Value {
    job.get("metadata")
        .cloned()
        .filter(Value::is_object)
        .unwrap_or_else(|| json!({}))
}

pub(crate) fn string_field(metadata: &Value, key: &str) -> Option<String> {
    metadata
        .get(key)
        .and_then(Value::as_str)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

pub(crate) fn u8_field(metadata: &Value, key: &str) -> Option<u8> {
    metadata
        .get(key)
        .and_then(Value::as_u64)
        .and_then(|value| u8::try_from(value).ok())
}

pub(crate) fn upload_file_from_path(path: &FsPath) -> Result<UploadFile, String> {
    let name = path
        .file_name()
        .and_then(|name| name.to_str())
        .map(ToString::to_string)
        .unwrap_or_else(|| "image.png".to_string());
    let bytes = fs::read(path).map_err(|error| format!("读取原始图片失败:{error}"))?;
    Ok(UploadFile { name, bytes })
}

pub(crate) fn generate_request_from_job(job: &Value) -> Result<GenerateRequest, String> {
    let mut request: GenerateRequest =
        serde_json::from_value(metadata_object(job)).map_err(|error| error.to_string())?;
    if request.provider.is_none() {
        request.provider = job
            .get("provider")
            .and_then(Value::as_str)
            .filter(|provider| !provider.is_empty())
            .map(ToString::to_string);
    }
    if request.prompt.trim().is_empty() {
        return Err("这个生成任务缺少 prompt,无法原样重试。".to_string());
    }
    Ok(request)
}

pub(crate) fn sorted_ref_inputs(dir: &FsPath) -> Result<Vec<UploadFile>, String> {
    let mut paths = fs::read_dir(dir)
        .map_err(|error| format!("读取原任务目录失败:{error}"))?
        .filter_map(Result::ok)
        .map(|entry| entry.path())
        .filter(|path| {
            path.is_file()
                && path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .is_some_and(|name| name.starts_with("ref-"))
        })
        .collect::<Vec<_>>();
    paths.sort();
    paths
        .iter()
        .map(|path| upload_file_from_path(path))
        .collect()
}

pub(crate) fn edit_job_input_dir(job_id: &str) -> Result<(PathBuf, Vec<UploadFile>), String> {
    let config = load_config_or_default();
    let dirs = [
        product_result_library_dir(Some(&config), ProductRuntime::DockerWeb).join(job_id),
        legacy_jobs_dir(Some(&config)).join(job_id),
    ];
    let mut last_error = None;
    for dir in dirs {
        match sorted_ref_inputs(&dir) {
            Ok(refs) if !refs.is_empty() => return Ok((dir, refs)),
            Ok(_) => {}
            Err(error) => last_error = Some(error),
        }
    }
    Err(last_error.unwrap_or_else(|| "这个编辑任务缺少原始参考图,无法原样重试。".to_string()))
}

pub(crate) fn edit_request_from_job(job_id: &str, job: &Value) -> Result<EditRequest, String> {
    let metadata = metadata_object(job);
    let (dir, refs) = edit_job_input_dir(job_id)?;
    let mask = {
        let path = dir.join("mask.png");
        if path.is_file() {
            Some(upload_file_from_path(&path)?)
        } else {
            None
        }
    };
    let selection_hint = {
        let path = dir.join("selection-hint.png");
        if path.is_file() {
            Some(upload_file_from_path(&path)?)
        } else {
            None
        }
    };
    Ok(EditRequest {
        prompt: string_field(&metadata, "prompt").unwrap_or_default(),
        provider: string_field(&metadata, "provider").or_else(|| {
            job.get("provider")
                .and_then(Value::as_str)
                .filter(|provider| !provider.is_empty())
                .map(ToString::to_string)
        }),
        size: string_field(&metadata, "size"),
        format: string_field(&metadata, "format"),
        quality: string_field(&metadata, "quality"),
        background: string_field(&metadata, "background"),
        n: u8_field(&metadata, "n"),
        compression: u8_field(&metadata, "compression"),
        input_fidelity: string_field(&metadata, "input_fidelity"),
        moderation: string_field(&metadata, "moderation"),
        storage_targets: metadata.get("storage_targets").and_then(|targets| {
            targets.as_array().map(|items| {
                items
                    .iter()
                    .filter_map(Value::as_str)
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
            })
        }),
        fallback_targets: metadata.get("fallback_targets").and_then(|targets| {
            targets.as_array().map(|items| {
                items
                    .iter()
                    .filter_map(Value::as_str)
                    .map(ToString::to_string)
                    .collect::<Vec<_>>()
            })
        }),
        refs,
        mask,
        selection_hint,
    })
}

pub(crate) async fn retry_job(
    Path(job_id): Path<String>,
    State(state): State<JobQueueState>,
) -> ApiResult {
    let job = show_history_job(&job_id)
        .map_err(app_error)
        .map_err(ApiError::not_found)?;
    match job.get("command").and_then(Value::as_str) {
        Some("images generate") => {
            let request = generate_request_from_job(&job).map_err(ApiError::bad_request)?;
            requested_n(request.n).map_err(ApiError::bad_request)?;
            let (id, dir) = unique_job_dir().map_err(ApiError::internal)?;
            let provider = selected_provider_name(request.provider.as_deref());
            let metadata = annotate_recovery_job_dir(
                serde_json::to_value(&request).unwrap_or_else(|_| json!({})),
                &dir,
            );
            enqueue_job(
                state,
                QueuedJob {
                    id,
                    command: "images generate".to_string(),
                    provider,
                    created_at: chrono_like_now(),
                    dir,
                    metadata,
                    task: QueuedTask::Generate(request),
                },
            )
            .map(Json)
            .map_err(ApiError::internal)
        }
        Some("images edit") => {
            let request = edit_request_from_job(&job_id, &job).map_err(ApiError::bad_request)?;
            if request.prompt.trim().is_empty() {
                return Err(ApiError::bad_request(
                    "这个编辑任务缺少 prompt,无法原样重试。",
                ));
            }
            requested_n(request.n).map_err(ApiError::bad_request)?;
            let (id, dir) = unique_job_dir().map_err(ApiError::internal)?;
            let provider = selected_provider_name(request.provider.as_deref());
            let metadata = annotate_recovery_job_dir(edit_request_metadata(&request), &dir);
            enqueue_job(
                state,
                QueuedJob {
                    id,
                    command: "images edit".to_string(),
                    provider,
                    created_at: chrono_like_now(),
                    dir,
                    metadata,
                    task: QueuedTask::Edit(request),
                },
            )
            .map(Json)
            .map_err(ApiError::internal)
        }
        _ => Err(ApiError::bad_request("这个任务类型暂不支持重试。")),
    }
}