@sebbo2002/to-paprika 1.0.1-develop.2 → 1.1.0-develop.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var _=Object.create;var $=Object.defineProperty;var J=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var G=Object.getPrototypeOf,q=Object.prototype.hasOwnProperty;var g=(o,e)=>()=>(o&&(e=o(o=0)),e);var D=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of H(e))!q.call(o,i)&&i!==t&&$(o,i,{get:()=>e[i],enumerable:!(r=J(e,i))||r.enumerable});return o};var c=(o,e,t)=>(t=o!=null?_(G(o)):{},D(e||!o||!o.__esModule?$(t,"default",{value:o,enumerable:!0}):t,o));var k,y,h,u,w,a,d=g(()=>{"use strict";k=require("crypto"),y=require("fs"),h=require("fs/promises"),u=require("path"),w=c(require("prompts"),1),a=class o{constructor(e){this.config=e}static get path(){return process.env.TO_PAPRIKA_CONFIG_PATH?(0,u.resolve)(process.env.TO_PAPRIKA_CONFIG_PATH):(0,u.join)(this.homePath,".to-paprika.config.json")}static get homePath(){if(process.platform==="win32"&&process.env.USERPROFILE)return process.env.USERPROFILE;if(process.platform!=="win32"&&process.env.HOME)return process.env.HOME;throw new Error("Cannot determine home directory, please set the TO_PAPRIKA_CONFIG_PATH environment variable.")}get llm(){return{...this.config.llm,prompt:process.env.TO_PAPRIKA_CONFIG_PROMPT||"You digitize recipes. Please recognize the recipe sent in the attachment and respond with a precise and complete JSON. This includes all original information such as the title, ingredients, preparation times, cooking steps, or notes. If the title is in all caps, it will be adjusted for better readability. If the source is available, such as the book title (possibly with page number) or the URL, this will also be included. Ensure that the JSON is properly formatted and valid. If any field is missing in the input, use undefined or an empty array as appropriate."}}get output(){return this.config.output}get server(){return this.config.server}static async getJson(){if(!(0,y.existsSync)(this.path))throw new Error(`Config file not found at ${this.path}. Run 'to-paprika setup' to create one.`);let e;try{e=await(0,h.readFile)(this.path,"utf8")}catch(r){throw new Error(`Failed to read config file at ${this.path}: ${r.message}`)}let t;try{t=JSON.parse(e)}catch(r){throw new Error(`Failed to parse config file at ${this.path}: ${r.message}`)}return t}static async setup(){console.log(`
2
+ "use strict";var J=Object.create;var $=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var q=Object.getPrototypeOf,D=Object.prototype.hasOwnProperty;var g=(o,e)=>()=>(o&&(e=o(o=0)),e);var W=(o,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of G(e))!D.call(o,i)&&i!==t&&$(o,i,{get:()=>e[i],enumerable:!(r=H(e,i))||r.enumerable});return o};var p=(o,e,t)=>(t=o!=null?J(q(o)):{},W(e||!o||!o.__esModule?$(t,"default",{value:o,enumerable:!0}):t,o));var k,y,h,u,w,a,d=g(()=>{"use strict";k=require("crypto"),y=require("fs"),h=require("fs/promises"),u=require("path"),w=p(require("prompts"),1),a=class o{constructor(e){this.config=e}static get path(){return process.env.TO_PAPRIKA_CONFIG_PATH?(0,u.resolve)(process.env.TO_PAPRIKA_CONFIG_PATH):(0,u.join)(this.homePath,".to-paprika.config.json")}static get homePath(){if(process.platform==="win32"&&process.env.USERPROFILE)return process.env.USERPROFILE;if(process.platform!=="win32"&&process.env.HOME)return process.env.HOME;throw new Error("Cannot determine home directory, please set the TO_PAPRIKA_CONFIG_PATH environment variable.")}get llm(){return{...this.config.llm,prompt:process.env.TO_PAPRIKA_CONFIG_PROMPT||"You digitize recipes. Please recognize the recipe sent in the attachment and respond with a precise and complete JSON. This includes all original information such as the title, ingredients, preparation times, cooking steps, or notes. If the title is in all caps, it will be adjusted for better readability. If the source is available, such as the book title (possibly with page number) or the URL, this will also be included. Ensure that the JSON is properly formatted and valid. If any field is missing in the input, use undefined or an empty array as appropriate."}}get output(){return this.config.output}get server(){return this.config.server}static async getJson(){if(!(0,y.existsSync)(this.path))throw new Error(`Config file not found at ${this.path}. Run 'to-paprika setup' to create one.`);let e;try{e=await(0,h.readFile)(this.path,"utf8")}catch(r){throw new Error(`Failed to read config file at ${this.path}: ${r.message}`)}let t;try{t=JSON.parse(e)}catch(r){throw new Error(`Failed to parse config file at ${this.path}: ${r.message}`)}return t}static async setup(){console.log(`
3
3
  \u{1F44B}\u{1F3FC} Hi there`),console.log("");let e;try{e=await this.getJson()}catch{}console.log("\u{1F325}\uFE0F to-paprica requires an OpenAI-compatible LLM provider to function."),console.log(" Please provide the following provider details:"),console.log("");let t=await(0,w.default)([{initial:e?.llm.baseUrl,message:" LLM Provider Base URL:",name:"baseUrl",type:"text",validate:n=>{try{return new URL(n),!0}catch{return"Invalid Base URL"}}},{initial:e?.llm.apiKey,message:" LLM Provider API Key:",name:"apiKey",type:"password",validate:n=>n.length>0?!0:"API Key cannot be empty"},{initial:e?.llm.model,message:" LLM Model to use:",name:"model",type:"text",validate:n=>n.length>0?!0:"Model cannot be empty"}]);console.log(""),console.log("\u{1F4C2} Next, please specify the output directory for your Paprika recipes."),console.log(" This is where all converted recipes will be saved."),console.log("");let r=await(0,w.default)({initial:e?.output||(0,u.resolve)(process.cwd()),message:" Output Directory:",name:"output",type:"text",validate:n=>(0,y.existsSync)(n)}),i={llm:t,output:r.output,server:{authToken:e?.server?.authToken}};i.server.authToken||(i.server.authToken=(0,k.randomBytes)(32).toString("hex")),console.log(""),console.log("\u{1F512} Great. Just in case you would like to use the build-in server,"),console.log(` please use this API Token to authenticate your requests:
4
- `),console.log(` ${i.server.authToken}`),console.log("");try{await(0,h.writeFile)(this.path,JSON.stringify(i,null,2))}catch(n){throw console.log("\u{1F6A8} Failed to save config file"),console.log(""),new Error(`Failed to write config file at ${this.path}: ${n.message}`)}console.log(""),console.log("\u{1F389} Config file saved successfully"),console.log("")}static async use(){let e=await this.getJson();return new o(e)}}});async function B(o){let e=(0,I.default)("zip",{zlib:{level:9}}),t=new x.PassThrough,r=[];t.on("data",i=>r.push(i)),t.on("end",()=>{}),e.pipe(t);for(let i of o){let n=Buffer.from(JSON.stringify({...i,servings:W(i.servings)}));e.append(n,{name:`${i.name}.paprikarecipe`})}return new Promise((i,n)=>{e.on("error",n),e.on("end",()=>{i(Buffer.concat(r))}),e.finalize()})}function W(o){if(!o)return null;if(/^[0-9]+ ?[mlg]{1,2}$/i.test(o.trim()))return o;let e=parseInt(o,10);return isNaN(e)?o:e.toString()}var I,x,s,b,E=g(()=>{"use strict";I=c(require("archiver"),1),x=require("stream"),s=c(require("zod"),1),b=s.object({cook_time:s.string().optional().nullable().describe("Cooking time required, e.g. '30 min' or '1 h'. Keep it short."),description:s.string().optional().nullable().describe("A brief description of the recipe. Use newline characters for formatting."),directions:s.string().describe("Step-by-step instructions for preparing the recipe. Use newline characters to separate steps, but do not remove any existing bullets or numbers from a list."),ingredients:s.string('Ingredients must be a string with each ingredient on a new line. Add required quantity at the front, e.g "1 cup milk" or "3 apples". Use markdown formatting to improve readability, for example to separate ingredients for dough and topping.'),name:s.string(),notes:s.string().describe("An unconstrained field for providing miscellaneous notes. Use newline characters for formatting. Do not add stuff that belongs in other fields."),prep_time:s.string().optional().nullable().describe('Preparation time required before cooking, e.g. "15 min" or "30 min". Keep it short.'),servings:s.string().optional().nullable().describe('Number of servings, e.g. "2" or "4-6 servings".'),source:s.string().optional().nullable().describe("Recipe source, e.g. 'Grandma's Cookbook, p. 123' or a URL."),total_time:s.string().optional().nullable().describe('Total time required to prepare the dish, e.g. "45 min" or "1.5 h". Keep it short.')})});var L,v,F,m,z,S,A,Y,l,C=g(()=>{"use strict";L=c(require("heic-convert"),1),v=require("fs"),F=require("fs/promises"),m=require("path"),z=c(require("openai"),1),S=require("openai/helpers/zod"),A=require("pdf-to-png-converter");d();E();Y=new Map([[".heic","image/heic"],[".jpeg","image/jpeg"],[".jpg","image/jpeg"],[".pdf","application/pdf"],[".png","image/png"],[".webp","image/webp"]]),l=class o{client;config;options;constructor(e,t={}){this.config=e,this.options=t,this.client=new z.default({apiKey:e.llm.apiKey,baseURL:e.llm.baseUrl})}static async convert(e,t,r){let i=await a.use(),n=[];if(typeof e=="string"&&(typeof t=="object"||t===void 0))n=await new o(i,t).convertFiles([e]);else if(Array.isArray(e)&&(typeof t=="object"||t===void 0))n=await new o(i,t).convertFiles(e);else if(e instanceof Buffer&&typeof t=="string")n=await new o(i,r).convertBuffer(`Buffer (${e.byteLength} bytes)`,e,t);else throw new Error("Invalid arguments for convert function.");return await B(n)}static async generateOutputFilePath(e){let t=`${Date.now().toString()}`;e.length===1&&(t=(0,m.basename)(e[0]).replace(/\.[a-z]{3,4}$/g,""));let r=await a.use(),i=(0,m.join)(r.output,t+".paprikarecipes");if((0,v.existsSync)(i)){let n=1;do i=(0,m.join)(r.output,`${t}-${n}.paprikarecipes`),n++;while((0,v.existsSync)(i))}return i}async convertBuffer(e,t,r){if(r==="image/heic"){this.log(e,"\u{1F500}");let f=Buffer.from(await(0,L.default)({buffer:t,format:"JPEG",quality:1}));return this.convertBuffer(e,f,"image/jpeg")}if(r==="application/pdf"){this.log(e,"\u{1F500}");let f=await(0,A.pdfToPng)(t);this.log(e,!0);let T=[];for(let O of f)T.push(...await this.convertBuffer(`${e} (p. ${O.pageNumber})`,O.content,"image/png"));return T}this.log(e,"\u{1FA84}");let n=(await this.client.chat.completions.create({messages:[{content:[{text:this.config.llm.prompt,type:"text"}],role:"developer"},{content:[{image_url:{url:`data:${r};base64,${t.toString("base64")}`},type:"image_url"}],role:"user"}],model:this.config.llm.model,response_format:(0,S.zodResponseFormat)(b,"recipe")})).choices[0];if(!n.message||!n.message.content)throw new Error(`Invalid response from LLM for file: ${e} (mime = ${r}, length = ${t.length})`);let p;try{p=JSON.parse(n.message.content)}catch(f){throw new Error(`Failed to parse LLM response for file: ${e} (mime = ${r}, length = ${t.length}): ${f.message}`)}let M=b.parse(p);return this.log(e,!0),[M]}async convertFile(e){let t=e.slice(e.lastIndexOf(".")).toLowerCase(),r=Y.get(t);if(r)return this.convertBuffer(e,await(0,F.readFile)(e),r);throw new Error(`Unsupported file format for file: ${e}`)}async convertFiles(e=[]){if(!e.length)throw new Error("No input files specified for conversion.");for(let r of e)if(!(0,v.existsSync)(r))throw new Error(`Unable to find file: ${r}`);let t=[];for(let r of e)t.push(...await this.convertFile(r));return t}log(e,t){let r=t===!0?"\u2705":t;this.options.stdout?.write(`\r${r} ${e}${t===!0?`
5
- `:""}`)}}});var Q={};var P,R,N=g(()=>{"use strict";P=c(require("express"),1);d();C();R=class o{app;server;constructor(){this.app=(0,P.default)(),this.app.use(P.default.raw({limit:"50mb",type:["image/jpeg","image/png","image/webp","image/heic"]})),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop())}static run(){new o}setupRoutes(){this.app.get("/ping",(t,r)=>{r.send("pong")});let e=null;a.use().then(t=>{e=t.server.authToken||null}),this.app.post("/convert",(t,r)=>{let i=t.headers.authorization||"";if(e&&i!==`Bearer ${e}`){r.sendStatus(401);return}let n=t.headers["content-type"]||"";l.convert(Buffer.from(t.body),n).then(p=>{r.setHeader("Content-Type","application/octet-stream"),r.setHeader("Content-Length",p.byteLength),r.setHeader("Content-Disposition",'attachment; filename="recipes.paprikarecipes"'),r.send(p)}).catch(p=>{console.error(p),r.sendStatus(500)})})}async stop(){await new Promise(e=>this.server.close(e)),process.exit()}};R.run()});var U=require("fs/promises"),j=c(require("yargs"),1),K=require("yargs/helpers");d();C();(0,j.default)((0,K.hideBin)(process.argv)).usage("$0 <cmd> [args]").command("setup","Create or update the configuration file",()=>{},()=>a.setup()).command("convert [files..]","Convert supported files to a .paprikarecipes file",o=>{o.positional("files",{array:!0,demandOption:!0,describe:"Input files to convert",type:"string"})},async o=>{let e=o.files,t=await l.convert(e,{stdout:process.stdout}),r=await l.generateOutputFilePath(e);await(0,U.writeFile)(r,t)}).command("server","Launch the server",()=>{},()=>{Promise.resolve().then(()=>(N(),Q)).catch(o=>{console.error("Failed to start the server:",o),process.exit(1)})}).demandCommand(1,1).strict().help("h").parse();
4
+ `),console.log(` ${i.server.authToken}`),console.log("");try{await(0,h.writeFile)(this.path,JSON.stringify(i,null,2))}catch(n){throw console.log("\u{1F6A8} Failed to save config file"),console.log(""),new Error(`Failed to write config file at ${this.path}: ${n.message}`)}console.log(""),console.log("\u{1F389} Config file saved successfully"),console.log("")}static async use(){let e=await this.getJson();return new o(e)}}});async function B(o){let e=(0,I.default)("zip",{zlib:{level:9}}),t=new x.PassThrough,r=[];t.on("data",i=>r.push(i)),t.on("end",()=>{}),e.pipe(t);for(let i of o){let n=Buffer.from(JSON.stringify({...i,servings:Y(i.servings)}));e.append(n,{name:`${i.name}.paprikarecipe`})}return new Promise((i,n)=>{e.on("error",n),e.on("end",()=>{i(Buffer.concat(r))}),e.finalize()})}function Y(o){if(!o)return null;if(/^[0-9]+ ?[mlg]{1,2}$/i.test(o.trim()))return o;let e=parseInt(o,10);return isNaN(e)?o:e.toString()}var I,x,s,b,E=g(()=>{"use strict";I=p(require("archiver"),1),x=require("stream"),s=p(require("zod"),1),b=s.object({cook_time:s.string().optional().nullable().describe("Cooking time required, e.g. '30 min' or '1 h'. Keep it short."),description:s.string().optional().nullable().describe("A brief description of the recipe. Use newline characters for formatting."),directions:s.string().describe("Step-by-step instructions for preparing the recipe. Use newline characters to separate steps, but do not remove any existing bullets or numbers from a list."),ingredients:s.string('Ingredients must be a string with each ingredient on a new line. Add required quantity at the front, e.g "1 cup milk" or "3 apples". Use markdown formatting to improve readability, for example to separate ingredients for dough and topping.'),name:s.string(),notes:s.string().describe("An unconstrained field for providing miscellaneous notes. Use newline characters for formatting. Do not add stuff that belongs in other fields."),prep_time:s.string().optional().nullable().describe('Preparation time required before cooking, e.g. "15 min" or "30 min". Keep it short.'),servings:s.string().optional().nullable().describe('Number of servings, e.g. "2" or "4-6 servings".'),source:s.string().optional().nullable().describe("Recipe source, e.g. 'Grandma's Cookbook, p. 123' or a URL."),total_time:s.string().optional().nullable().describe('Total time required to prepare the dish, e.g. "45 min" or "1.5 h". Keep it short.')})});var F,v,L,m,z,S,A,Q,l,C=g(()=>{"use strict";F=p(require("heic-convert"),1),v=require("fs"),L=require("fs/promises"),m=require("path"),z=p(require("openai"),1),S=require("openai/helpers/zod"),A=require("pdf-to-png-converter");d();E();Q=new Map([[".heic","image/heic"],[".jpeg","image/jpeg"],[".jpg","image/jpeg"],[".pdf","application/pdf"],[".png","image/png"],[".webp","image/webp"]]),l=class o{client;config;options;constructor(e,t={}){this.config=e,this.options=t,this.client=new z.default({apiKey:e.llm.apiKey,baseURL:e.llm.baseUrl})}static async convert(e,t,r){let i=await a.use(),n=[];if(typeof e=="string"&&(typeof t=="object"||t===void 0))n=await new o(i,t).convertFiles([e]);else if(Array.isArray(e)&&(typeof t=="object"||t===void 0))n=await new o(i,t).convertFiles(e);else if(e instanceof Buffer&&typeof t=="string")n=await new o(i,r).convertBuffer(`Buffer (${e.byteLength} bytes)`,e,t);else throw new Error("Invalid arguments for convert function.");return await B(n)}static async generateOutputFilePath(e){let t=`${Date.now().toString()}`;e.length===1&&(t=(0,m.basename)(e[0]).replace(/\.[a-z]{3,4}$/g,""));let r=await a.use(),i=(0,m.join)(r.output,t+".paprikarecipes");if((0,v.existsSync)(i)){let n=1;do i=(0,m.join)(r.output,`${t}-${n}.paprikarecipes`),n++;while((0,v.existsSync)(i))}return i}async convertBuffer(e,t,r){if(r==="image/heic"){this.log(e,"\u{1F500}");let f=Buffer.from(await(0,F.default)({buffer:t,format:"JPEG",quality:1}));return this.convertBuffer(e,f,"image/jpeg")}if(r==="application/pdf"){this.log(e,"\u{1F500}");let f=await(0,A.pdfToPng)(t);this.log(e,!0);let T=[];for(let O of f)T.push(...await this.convertBuffer(`${e} (p. ${O.pageNumber})`,O.content,"image/png"));return T}this.log(e,"\u{1FA84}");let n=(await this.client.chat.completions.create({messages:[{content:[{text:this.config.llm.prompt,type:"text"}],role:"developer"},{content:[{image_url:{url:`data:${r};base64,${t.toString("base64")}`},type:"image_url"}],role:"user"}],model:this.config.llm.model,response_format:(0,S.zodResponseFormat)(b,"recipe")})).choices[0];if(!n.message||!n.message.content)throw new Error(`Invalid response from LLM for file: ${e} (mime = ${r}, length = ${t.length})`);let c;try{c=JSON.parse(n.message.content)}catch(f){throw new Error(`Failed to parse LLM response for file: ${e} (mime = ${r}, length = ${t.length}): ${f.message}`)}let _=b.parse(c);return this.log(e,!0),[_]}async convertFile(e){let t=e.slice(e.lastIndexOf(".")).toLowerCase(),r=Q.get(t);if(r)return this.convertBuffer(e,await(0,L.readFile)(e),r);throw new Error(`Unsupported file format for file: ${e}`)}async convertFiles(e=[]){if(!e.length)throw new Error("No input files specified for conversion.");for(let r of e)if(!(0,v.existsSync)(r))throw new Error(`Unable to find file: ${r}`);let t=[];for(let r of e)t.push(...await this.convertFile(r));return t}log(e,t){let r=t===!0?"\u2705":t;this.options.stdout?.write(`\r${r} ${e}${t===!0?`
5
+ `:""}`)}}});var V={};var P,R,N=g(()=>{"use strict";P=p(require("express"),1);d();C();R=class o{app;server;constructor(){this.app=(0,P.default)(),this.app.use(P.default.raw({limit:"50mb",type:["image/jpeg","image/png","image/webp","image/heic"]})),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop())}static run(){new o}setupRoutes(){this.app.get("/ping",(t,r)=>{r.send("pong")});let e=null;a.use().then(t=>{e=t.server.authToken||null}),this.app.post("/convert",(t,r)=>{let i=t.headers.authorization||"";if(e&&i!==`Bearer ${e}`){r.sendStatus(401);return}let n=t.headers["content-type"]||"";l.convert(Buffer.from(t.body),n).then(c=>{r.setHeader("Content-Type","application/octet-stream"),r.setHeader("Content-Length",c.byteLength),r.setHeader("Content-Disposition",'attachment; filename="recipes.paprikarecipes"'),r.send(c)}).catch(c=>{console.error(c),r.sendStatus(500)})})}async stop(){await new Promise(e=>this.server.close(e)),process.exit()}};R.run()});var U=require("fs/promises"),j=p(require("reveal-file"),1),K=p(require("yargs"),1),M=require("yargs/helpers");d();C();(0,K.default)((0,M.hideBin)(process.argv)).usage("$0 <cmd> [args]").command("setup","Create or update the configuration file",()=>{},()=>a.setup()).command("convert [files..]","Convert supported files to a .paprikarecipes file",o=>{o.positional("files",{array:!0,demandOption:!0,describe:"Input files to convert",type:"string"})},async o=>{let e=o.files,t=await l.convert(e,{stdout:process.stdout}),r=await l.generateOutputFilePath(e);await(0,U.writeFile)(r,t),await(0,j.default)(r)}).command("server","Launch the server",()=>{},()=>{Promise.resolve().then(()=>(N(),V)).catch(o=>{console.error("Failed to start the server:",o),process.exit(1)})}).demandCommand(1,1).strict().help("h").parse();
6
6
  //# sourceMappingURL=cli.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/config.ts","../../src/lib/recipe.ts","../../src/lib/convert.ts","../../src/bin/start.ts","../../src/bin/cli.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport prompts from 'prompts';\n\nexport interface ConfigContent {\n llm: {\n apiKey: string;\n baseUrl: string;\n model: string;\n };\n output: string;\n server: {\n authToken?: string;\n };\n}\n\nexport class Config {\n static get path() {\n if (process.env.TO_PAPRIKA_CONFIG_PATH) {\n return resolve(process.env.TO_PAPRIKA_CONFIG_PATH);\n }\n\n return join(this.homePath, '.to-paprika.config.json');\n }\n protected static get homePath() {\n if (process.platform === 'win32' && process.env.USERPROFILE) {\n return process.env.USERPROFILE;\n }\n if (process.platform !== 'win32' && process.env.HOME) {\n return process.env.HOME;\n }\n\n throw new Error(\n 'Cannot determine home directory, please set the TO_PAPRIKA_CONFIG_PATH environment variable.',\n );\n }\n\n get llm(): ConfigContent['llm'] & { prompt: string } {\n return {\n ...this.config.llm,\n prompt:\n process.env.TO_PAPRIKA_CONFIG_PROMPT ||\n 'You digitize recipes. Please recognize the recipe sent in the attachment and respond with a precise and complete JSON. This includes all original information such as the title, ingredients, preparation times, cooking steps, or notes. If the title is in all caps, it will be adjusted for better readability. If the source is available, such as the book title (possibly with page number) or the URL, this will also be included. Ensure that the JSON is properly formatted and valid. If any field is missing in the input, use undefined or an empty array as appropriate.',\n };\n }\n get output() {\n return this.config.output;\n }\n get server(): ConfigContent['server'] {\n return this.config.server;\n }\n\n constructor(private readonly config: ConfigContent) {}\n\n static async getJson() {\n if (!existsSync(this.path)) {\n throw new Error(\n `Config file not found at ${this.path}. Run 'to-paprika setup' to create one.`,\n );\n }\n\n let content: string | undefined;\n try {\n content = await readFile(this.path, 'utf8');\n } catch (err) {\n throw new Error(\n `Failed to read config file at ${this.path}: ${(err as Error).message}`,\n );\n }\n\n let json: ConfigContent;\n try {\n json = JSON.parse(content) as ConfigContent;\n } catch (err) {\n throw new Error(\n `Failed to parse config file at ${this.path}: ${(err as Error).message}`,\n );\n }\n\n return json;\n }\n\n static async setup() {\n console.log('\\nšŸ‘‹šŸ¼ Hi there');\n console.log('');\n\n let json: ConfigContent | undefined = undefined;\n try {\n json = await this.getJson();\n } catch {\n // ignore errors\n }\n\n console.log(\n 'šŸŒ„ļø to-paprica requires an OpenAI-compatible LLM provider to function.',\n );\n console.log(' Please provide the following provider details:');\n console.log('');\n\n const llm = await prompts([\n {\n initial: json?.llm.baseUrl,\n message: ' LLM Provider Base URL:',\n name: 'baseUrl',\n type: 'text',\n validate: (value) => {\n try {\n new URL(value);\n return true;\n } catch {\n return 'Invalid Base URL';\n }\n },\n },\n {\n initial: json?.llm.apiKey,\n message: ' LLM Provider API Key:',\n name: 'apiKey',\n type: 'password',\n validate: (value) =>\n value.length > 0 ? true : 'API Key cannot be empty',\n },\n {\n initial: json?.llm.model,\n message: ' LLM Model to use:',\n name: 'model',\n type: 'text',\n validate: (value) =>\n value.length > 0 ? true : 'Model cannot be empty',\n },\n ]);\n\n console.log('');\n console.log(\n 'šŸ“‚ Next, please specify the output directory for your Paprika recipes.',\n );\n console.log(' This is where all converted recipes will be saved.');\n console.log('');\n\n const output = await prompts({\n initial: json?.output || resolve(process.cwd()),\n message: ' Output Directory:',\n name: 'output',\n type: 'text',\n validate: (value) => existsSync(value),\n });\n\n const config: ConfigContent = {\n llm,\n output: output.output,\n server: {\n authToken: json?.server?.authToken,\n },\n };\n if (!config.server.authToken) {\n config.server.authToken = randomBytes(32).toString('hex');\n }\n\n console.log('');\n console.log(\n 'šŸ”’ Great. Just in case you would like to use the build-in server,',\n );\n console.log(\n ' please use this API Token to authenticate your requests:\\n',\n );\n console.log(` ${config.server.authToken}`);\n console.log('');\n\n try {\n await writeFile(this.path, JSON.stringify(config, null, 2));\n } catch (error) {\n console.log('🚨 Failed to save config file');\n console.log('');\n throw new Error(\n `Failed to write config file at ${this.path}: ${(error as Error).message}`,\n );\n }\n\n console.log('');\n console.log('šŸŽ‰ Config file saved successfully');\n console.log('');\n }\n\n static async use() {\n const json = await this.getJson();\n return new Config(json);\n }\n}\n","import archiver from 'archiver';\nimport { PassThrough } from 'node:stream';\nimport * as z from 'zod';\n\nexport const Recipe = z.object({\n cook_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n \"Cooking time required, e.g. '30 min' or '1 h'. Keep it short.\",\n ),\n description: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'A brief description of the recipe. Use newline characters for formatting.',\n ),\n directions: z\n .string()\n .describe(\n 'Step-by-step instructions for preparing the recipe. Use newline characters to separate steps, but do not remove any existing bullets or numbers from a list.',\n ),\n ingredients: z.string(\n 'Ingredients must be a string with each ingredient on a new line. Add required quantity at the front, e.g \"1 cup milk\" or \"3 apples\". Use markdown formatting to improve readability, for example to separate ingredients for dough and topping.',\n ),\n name: z.string(),\n notes: z\n .string()\n .describe(\n 'An unconstrained field for providing miscellaneous notes. Use newline characters for formatting. Do not add stuff that belongs in other fields.',\n ),\n prep_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'Preparation time required before cooking, e.g. \"15 min\" or \"30 min\". Keep it short.',\n ),\n servings: z\n .string()\n .optional()\n .nullable()\n .describe('Number of servings, e.g. \"2\" or \"4-6 servings\".'),\n source: z\n .string()\n .optional()\n .nullable()\n .describe(\"Recipe source, e.g. 'Grandma's Cookbook, p. 123' or a URL.\"),\n total_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'Total time required to prepare the dish, e.g. \"45 min\" or \"1.5 h\". Keep it short.',\n ),\n});\n\nexport type RecipeType = z.infer<typeof Recipe>;\n\n/**\n * Convert an array of recipes to a .paprikarecipes buffer\n */\nexport async function toRecipes(recipes: RecipeType[]): Promise<Buffer> {\n const zip = archiver('zip', { zlib: { level: 9 } });\n const zipStream = new PassThrough();\n const chunks: Buffer[] = [];\n\n zipStream.on('data', (chunk) => chunks.push(chunk));\n zipStream.on('end', () => {});\n zip.pipe(zipStream);\n\n for (const recipe of recipes) {\n const data = Buffer.from(\n JSON.stringify({\n ...recipe,\n servings: toServings(recipe.servings),\n }),\n );\n zip.append(data, { name: `${recipe.name}.paprikarecipe` });\n }\n\n return new Promise((resolve, reject) => {\n zip.on('error', reject);\n zip.on('end', () => {\n resolve(Buffer.concat(chunks));\n });\n zip.finalize();\n });\n}\nfunction toServings(servings: null | string | undefined): null | string {\n if (!servings) {\n return null;\n }\n if (/^[0-9]+ ?[mlg]{1,2}$/i.test(servings.trim())) {\n return servings;\n }\n\n const number = parseInt(servings, 10);\n if (!isNaN(number)) {\n return number.toString();\n }\n\n return servings;\n}\n","import type { Writable } from 'node:stream';\n\nimport convert from 'heic-convert';\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport OpenAI from 'openai';\nimport { zodResponseFormat } from 'openai/helpers/zod';\nimport { pdfToPng } from 'pdf-to-png-converter';\n\nimport { Config } from './config.js';\nimport { Recipe, type RecipeType, toRecipes } from './recipe.js';\n\nexport interface ConverterOptions {\n stdout?: Writable;\n}\n\nexport const mimeTypes = new Map([\n ['.heic', 'image/heic'],\n ['.jpeg', 'image/jpeg'],\n ['.jpg', 'image/jpeg'],\n ['.pdf', 'application/pdf'],\n ['.png', 'image/png'],\n ['.webp', 'image/webp'],\n]);\n\nexport class Converter {\n protected client: OpenAI;\n protected config: Config;\n protected options: ConverterOptions;\n\n protected constructor(config: Config, options: ConverterOptions = {}) {\n this.config = config;\n this.options = options;\n this.client = new OpenAI({\n apiKey: config.llm.apiKey,\n baseURL: config.llm.baseUrl,\n });\n }\n\n static async convert(\n data: Buffer,\n mimeType: string,\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n file: string,\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n files: string[],\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n arg1: Buffer | string | string[],\n arg2?: ConverterOptions | string,\n arg3?: ConverterOptions,\n ): Promise<Buffer> {\n const config = await Config.use();\n\n let recipes: RecipeType[] = [];\n if (\n typeof arg1 === 'string' &&\n (typeof arg2 === 'object' || arg2 === undefined)\n ) {\n recipes = await new Converter(config, arg2).convertFiles([arg1]);\n } else if (\n Array.isArray(arg1) &&\n (typeof arg2 === 'object' || arg2 === undefined)\n ) {\n recipes = await new Converter(config, arg2).convertFiles(arg1);\n } else if (arg1 instanceof Buffer && typeof arg2 === 'string') {\n recipes = await new Converter(config, arg3).convertBuffer(\n `Buffer (${arg1.byteLength} bytes)`,\n arg1,\n arg2,\n );\n } else {\n throw new Error('Invalid arguments for convert function.');\n }\n\n return await toRecipes(recipes);\n }\n\n static async generateOutputFilePath(inputFiles: string[]): Promise<string> {\n let fileName = `${Date.now().toString()}`;\n if (inputFiles.length === 1) {\n fileName = basename(inputFiles[0]).replace(/\\.[a-z]{3,4}$/g, '');\n }\n\n const config = await Config.use();\n let path = join(config.output, fileName + '.paprikarecipes');\n if (existsSync(path)) {\n let counter = 1;\n do {\n path = join(\n config.output,\n `${fileName}-${counter}.paprikarecipes`,\n );\n counter++;\n } while (existsSync(path));\n }\n\n return path;\n }\n\n protected async convertBuffer(\n title: string,\n data: Buffer,\n mimeType: string,\n ): Promise<RecipeType[]> {\n // heic handling\n if (mimeType === 'image/heic') {\n this.log(title, 'šŸ”€');\n const jpeg = Buffer.from(\n await convert({\n // @ts-expect-error Not working with ArrayBuffer, but works with Buffer???\n buffer: data,\n format: 'JPEG',\n quality: 1,\n }),\n );\n return this.convertBuffer(title, jpeg, 'image/jpeg');\n }\n\n // pdf handling\n if (mimeType === 'application/pdf') {\n this.log(title, 'šŸ”€');\n const pages = await pdfToPng(data);\n this.log(title, true);\n\n const result: RecipeType[] = [];\n for (const page of pages) {\n result.push(\n ...(await this.convertBuffer(\n `${title} (p. ${page.pageNumber})`,\n page.content,\n 'image/png',\n )),\n );\n }\n\n return result;\n }\n\n this.log(title, 'šŸŖ„');\n const response = await this.client.chat.completions.create({\n messages: [\n {\n content: [\n {\n text: this.config.llm.prompt,\n type: 'text',\n },\n ],\n role: 'developer',\n },\n {\n content: [\n {\n image_url: {\n url: `data:${mimeType};base64,${data.toString('base64')}`,\n },\n type: 'image_url',\n },\n ],\n role: 'user',\n },\n ],\n model: this.config.llm.model,\n response_format: zodResponseFormat(Recipe, 'recipe'),\n });\n\n const choice = response.choices[0];\n if (!choice.message || !choice.message.content) {\n throw new Error(\n `Invalid response from LLM for file: ${title} (mime = ${mimeType}, length = ${data.length})`,\n );\n }\n\n let json;\n try {\n json = JSON.parse(choice.message.content);\n } catch (error) {\n throw new Error(\n `Failed to parse LLM response for file: ${title} (mime = ${mimeType}, length = ${data.length}): ${(error as Error).message}`,\n );\n }\n\n const recipe = Recipe.parse(json);\n this.log(title, true);\n return [recipe];\n }\n\n protected async convertFile(file: string): Promise<RecipeType[]> {\n const ext = file.slice(file.lastIndexOf('.')).toLowerCase();\n const mimeType = mimeTypes.get(ext);\n if (mimeType) {\n return this.convertBuffer(file, await readFile(file), mimeType);\n }\n\n throw new Error(`Unsupported file format for file: ${file}`);\n }\n\n protected async convertFiles(files: string[] = []) {\n if (!files.length) {\n throw new Error('No input files specified for conversion.');\n }\n\n // Check all files exist\n for (const file of files) {\n if (!existsSync(file)) {\n throw new Error(`Unable to find file: ${file}`);\n }\n }\n\n // Convert files one by one\n const recipes: RecipeType[] = [];\n for (const file of files) {\n recipes.push(...(await this.convertFile(file)));\n }\n\n return recipes;\n }\n\n protected log(file: string, emojiOrDone: string | true) {\n const emoji = emojiOrDone === true ? 'āœ…' : emojiOrDone;\n this.options.stdout?.write(\n `\\r${emoji} ${file}${emojiOrDone === true ? '\\n' : ''}`,\n );\n }\n}\n","#!/usr/bin/env node\n'use strict';\n\nimport express, { type Express } from 'express';\nimport { Server } from 'http';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nclass AppServer {\n private app: Express;\n private server: Server;\n\n constructor() {\n this.app = express();\n this.app.use(\n express.raw({\n limit: '50mb',\n type: ['image/jpeg', 'image/png', 'image/webp', 'image/heic'],\n }),\n );\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n }\n\n static run() {\n new AppServer();\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n let authToken: null | string = null;\n Config.use().then((config) => {\n authToken = config.server.authToken || null;\n });\n\n this.app.post('/convert', (req, res) => {\n const authTokenHeader = req.headers['authorization'] || '';\n if (authToken && authTokenHeader !== `Bearer ${authToken}`) {\n res.sendStatus(401);\n return;\n }\n\n const contentType = req.headers['content-type'] || '';\n Converter.convert(Buffer.from(req.body), contentType)\n .then((response: Buffer) => {\n res.setHeader('Content-Type', 'application/octet-stream');\n res.setHeader('Content-Length', response.byteLength);\n res.setHeader(\n 'Content-Disposition',\n 'attachment; filename=\"recipes.paprikarecipes\"',\n );\n res.send(response);\n })\n .catch((error) => {\n console.error(error);\n res.sendStatus(500);\n });\n });\n\n // add additional routes\n }\n\n async stop() {\n await new Promise((cb) => this.server.close(cb));\n\n // await db.close() if we have a db connection in this app\n // await other things we should cleanup nicely\n\n process.exit();\n }\n}\n\nAppServer.run();\n","#!/usr/bin/env node\n'use strict';\n\nimport { writeFile } from 'node:fs/promises';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nyargs(hideBin(process.argv))\n .usage('$0 <cmd> [args]')\n .command(\n 'setup',\n 'Create or update the configuration file',\n () => {},\n () => Config.setup(),\n )\n .command(\n 'convert [files..]',\n 'Convert supported files to a .paprikarecipes file',\n (yargs) => {\n yargs.positional('files', {\n array: true,\n demandOption: true,\n describe: 'Input files to convert',\n type: 'string',\n });\n },\n async (argv) => {\n const files = argv.files as string[];\n const recipes = await Converter.convert(files, {\n stdout: process.stdout,\n });\n\n const outputFile = await Converter.generateOutputFilePath(files);\n await writeFile(outputFile, recipes);\n },\n )\n .command(\n 'server',\n 'Launch the server',\n () => {},\n () => {\n import('./start.ts').catch((error) => {\n console.error('Failed to start the server:', error);\n process.exit(1);\n });\n },\n )\n .demandCommand(1, 1)\n .strict()\n .help('h')\n .parse();\n"],"mappings":"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","names":["import_node_crypto","import_node_fs","import_promises","import_node_path","import_prompts","Config","init_config","__esmMin","_Config","config","content","err","json","llm","prompts","value","output","error","toRecipes","recipes","zip","archiver","zipStream","chunks","chunk","recipe","data","toServings","resolve","reject","servings","number","import_archiver","import_node_stream","z","Recipe","init_recipe","__esmMin","import_heic_convert","import_node_fs","import_promises","import_node_path","import_openai","import_zod","import_pdf_to_png_converter","mimeTypes","Converter","init_convert","__esmMin","init_config","init_recipe","_Converter","config","options","OpenAI","arg1","arg2","arg3","Config","recipes","toRecipes","inputFiles","fileName","path","counter","title","data","mimeType","jpeg","convert","pages","result","page","choice","Recipe","json","error","recipe","file","ext","files","emojiOrDone","emoji","start_exports","import_express","AppServer","init_start","__esmMin","init_config","init_convert","_AppServer","express","req","res","authToken","Config","config","authTokenHeader","contentType","Converter","response","error","cb","import_promises","import_yargs","import_helpers","init_config","init_convert","yargs","Config","argv","files","recipes","Converter","outputFile","error"]}
1
+ {"version":3,"sources":["../../src/lib/config.ts","../../src/lib/recipe.ts","../../src/lib/convert.ts","../../src/bin/start.ts","../../src/bin/cli.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { readFile, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport prompts from 'prompts';\n\nexport interface ConfigContent {\n llm: {\n apiKey: string;\n baseUrl: string;\n model: string;\n };\n output: string;\n server: {\n authToken?: string;\n };\n}\n\nexport class Config {\n static get path() {\n if (process.env.TO_PAPRIKA_CONFIG_PATH) {\n return resolve(process.env.TO_PAPRIKA_CONFIG_PATH);\n }\n\n return join(this.homePath, '.to-paprika.config.json');\n }\n protected static get homePath() {\n if (process.platform === 'win32' && process.env.USERPROFILE) {\n return process.env.USERPROFILE;\n }\n if (process.platform !== 'win32' && process.env.HOME) {\n return process.env.HOME;\n }\n\n throw new Error(\n 'Cannot determine home directory, please set the TO_PAPRIKA_CONFIG_PATH environment variable.',\n );\n }\n\n get llm(): ConfigContent['llm'] & { prompt: string } {\n return {\n ...this.config.llm,\n prompt:\n process.env.TO_PAPRIKA_CONFIG_PROMPT ||\n 'You digitize recipes. Please recognize the recipe sent in the attachment and respond with a precise and complete JSON. This includes all original information such as the title, ingredients, preparation times, cooking steps, or notes. If the title is in all caps, it will be adjusted for better readability. If the source is available, such as the book title (possibly with page number) or the URL, this will also be included. Ensure that the JSON is properly formatted and valid. If any field is missing in the input, use undefined or an empty array as appropriate.',\n };\n }\n get output() {\n return this.config.output;\n }\n get server(): ConfigContent['server'] {\n return this.config.server;\n }\n\n constructor(private readonly config: ConfigContent) {}\n\n static async getJson() {\n if (!existsSync(this.path)) {\n throw new Error(\n `Config file not found at ${this.path}. Run 'to-paprika setup' to create one.`,\n );\n }\n\n let content: string | undefined;\n try {\n content = await readFile(this.path, 'utf8');\n } catch (err) {\n throw new Error(\n `Failed to read config file at ${this.path}: ${(err as Error).message}`,\n );\n }\n\n let json: ConfigContent;\n try {\n json = JSON.parse(content) as ConfigContent;\n } catch (err) {\n throw new Error(\n `Failed to parse config file at ${this.path}: ${(err as Error).message}`,\n );\n }\n\n return json;\n }\n\n static async setup() {\n console.log('\\nšŸ‘‹šŸ¼ Hi there');\n console.log('');\n\n let json: ConfigContent | undefined = undefined;\n try {\n json = await this.getJson();\n } catch {\n // ignore errors\n }\n\n console.log(\n 'šŸŒ„ļø to-paprica requires an OpenAI-compatible LLM provider to function.',\n );\n console.log(' Please provide the following provider details:');\n console.log('');\n\n const llm = await prompts([\n {\n initial: json?.llm.baseUrl,\n message: ' LLM Provider Base URL:',\n name: 'baseUrl',\n type: 'text',\n validate: (value) => {\n try {\n new URL(value);\n return true;\n } catch {\n return 'Invalid Base URL';\n }\n },\n },\n {\n initial: json?.llm.apiKey,\n message: ' LLM Provider API Key:',\n name: 'apiKey',\n type: 'password',\n validate: (value) =>\n value.length > 0 ? true : 'API Key cannot be empty',\n },\n {\n initial: json?.llm.model,\n message: ' LLM Model to use:',\n name: 'model',\n type: 'text',\n validate: (value) =>\n value.length > 0 ? true : 'Model cannot be empty',\n },\n ]);\n\n console.log('');\n console.log(\n 'šŸ“‚ Next, please specify the output directory for your Paprika recipes.',\n );\n console.log(' This is where all converted recipes will be saved.');\n console.log('');\n\n const output = await prompts({\n initial: json?.output || resolve(process.cwd()),\n message: ' Output Directory:',\n name: 'output',\n type: 'text',\n validate: (value) => existsSync(value),\n });\n\n const config: ConfigContent = {\n llm,\n output: output.output,\n server: {\n authToken: json?.server?.authToken,\n },\n };\n if (!config.server.authToken) {\n config.server.authToken = randomBytes(32).toString('hex');\n }\n\n console.log('');\n console.log(\n 'šŸ”’ Great. Just in case you would like to use the build-in server,',\n );\n console.log(\n ' please use this API Token to authenticate your requests:\\n',\n );\n console.log(` ${config.server.authToken}`);\n console.log('');\n\n try {\n await writeFile(this.path, JSON.stringify(config, null, 2));\n } catch (error) {\n console.log('🚨 Failed to save config file');\n console.log('');\n throw new Error(\n `Failed to write config file at ${this.path}: ${(error as Error).message}`,\n );\n }\n\n console.log('');\n console.log('šŸŽ‰ Config file saved successfully');\n console.log('');\n }\n\n static async use() {\n const json = await this.getJson();\n return new Config(json);\n }\n}\n","import archiver from 'archiver';\nimport { PassThrough } from 'node:stream';\nimport * as z from 'zod';\n\nexport const Recipe = z.object({\n cook_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n \"Cooking time required, e.g. '30 min' or '1 h'. Keep it short.\",\n ),\n description: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'A brief description of the recipe. Use newline characters for formatting.',\n ),\n directions: z\n .string()\n .describe(\n 'Step-by-step instructions for preparing the recipe. Use newline characters to separate steps, but do not remove any existing bullets or numbers from a list.',\n ),\n ingredients: z.string(\n 'Ingredients must be a string with each ingredient on a new line. Add required quantity at the front, e.g \"1 cup milk\" or \"3 apples\". Use markdown formatting to improve readability, for example to separate ingredients for dough and topping.',\n ),\n name: z.string(),\n notes: z\n .string()\n .describe(\n 'An unconstrained field for providing miscellaneous notes. Use newline characters for formatting. Do not add stuff that belongs in other fields.',\n ),\n prep_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'Preparation time required before cooking, e.g. \"15 min\" or \"30 min\". Keep it short.',\n ),\n servings: z\n .string()\n .optional()\n .nullable()\n .describe('Number of servings, e.g. \"2\" or \"4-6 servings\".'),\n source: z\n .string()\n .optional()\n .nullable()\n .describe(\"Recipe source, e.g. 'Grandma's Cookbook, p. 123' or a URL.\"),\n total_time: z\n .string()\n .optional()\n .nullable()\n .describe(\n 'Total time required to prepare the dish, e.g. \"45 min\" or \"1.5 h\". Keep it short.',\n ),\n});\n\nexport type RecipeType = z.infer<typeof Recipe>;\n\n/**\n * Convert an array of recipes to a .paprikarecipes buffer\n */\nexport async function toRecipes(recipes: RecipeType[]): Promise<Buffer> {\n const zip = archiver('zip', { zlib: { level: 9 } });\n const zipStream = new PassThrough();\n const chunks: Buffer[] = [];\n\n zipStream.on('data', (chunk) => chunks.push(chunk));\n zipStream.on('end', () => {});\n zip.pipe(zipStream);\n\n for (const recipe of recipes) {\n const data = Buffer.from(\n JSON.stringify({\n ...recipe,\n servings: toServings(recipe.servings),\n }),\n );\n zip.append(data, { name: `${recipe.name}.paprikarecipe` });\n }\n\n return new Promise((resolve, reject) => {\n zip.on('error', reject);\n zip.on('end', () => {\n resolve(Buffer.concat(chunks));\n });\n zip.finalize();\n });\n}\nfunction toServings(servings: null | string | undefined): null | string {\n if (!servings) {\n return null;\n }\n if (/^[0-9]+ ?[mlg]{1,2}$/i.test(servings.trim())) {\n return servings;\n }\n\n const number = parseInt(servings, 10);\n if (!isNaN(number)) {\n return number.toString();\n }\n\n return servings;\n}\n","import type { Writable } from 'node:stream';\n\nimport convert from 'heic-convert';\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport OpenAI from 'openai';\nimport { zodResponseFormat } from 'openai/helpers/zod';\nimport { pdfToPng } from 'pdf-to-png-converter';\n\nimport { Config } from './config.js';\nimport { Recipe, type RecipeType, toRecipes } from './recipe.js';\n\nexport interface ConverterOptions {\n stdout?: Writable;\n}\n\nexport const mimeTypes = new Map([\n ['.heic', 'image/heic'],\n ['.jpeg', 'image/jpeg'],\n ['.jpg', 'image/jpeg'],\n ['.pdf', 'application/pdf'],\n ['.png', 'image/png'],\n ['.webp', 'image/webp'],\n]);\n\nexport class Converter {\n protected client: OpenAI;\n protected config: Config;\n protected options: ConverterOptions;\n\n protected constructor(config: Config, options: ConverterOptions = {}) {\n this.config = config;\n this.options = options;\n this.client = new OpenAI({\n apiKey: config.llm.apiKey,\n baseURL: config.llm.baseUrl,\n });\n }\n\n static async convert(\n data: Buffer,\n mimeType: string,\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n file: string,\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n files: string[],\n options?: ConverterOptions,\n ): Promise<Buffer>;\n static async convert(\n arg1: Buffer | string | string[],\n arg2?: ConverterOptions | string,\n arg3?: ConverterOptions,\n ): Promise<Buffer> {\n const config = await Config.use();\n\n let recipes: RecipeType[] = [];\n if (\n typeof arg1 === 'string' &&\n (typeof arg2 === 'object' || arg2 === undefined)\n ) {\n recipes = await new Converter(config, arg2).convertFiles([arg1]);\n } else if (\n Array.isArray(arg1) &&\n (typeof arg2 === 'object' || arg2 === undefined)\n ) {\n recipes = await new Converter(config, arg2).convertFiles(arg1);\n } else if (arg1 instanceof Buffer && typeof arg2 === 'string') {\n recipes = await new Converter(config, arg3).convertBuffer(\n `Buffer (${arg1.byteLength} bytes)`,\n arg1,\n arg2,\n );\n } else {\n throw new Error('Invalid arguments for convert function.');\n }\n\n return await toRecipes(recipes);\n }\n\n static async generateOutputFilePath(inputFiles: string[]): Promise<string> {\n let fileName = `${Date.now().toString()}`;\n if (inputFiles.length === 1) {\n fileName = basename(inputFiles[0]).replace(/\\.[a-z]{3,4}$/g, '');\n }\n\n const config = await Config.use();\n let path = join(config.output, fileName + '.paprikarecipes');\n if (existsSync(path)) {\n let counter = 1;\n do {\n path = join(\n config.output,\n `${fileName}-${counter}.paprikarecipes`,\n );\n counter++;\n } while (existsSync(path));\n }\n\n return path;\n }\n\n protected async convertBuffer(\n title: string,\n data: Buffer,\n mimeType: string,\n ): Promise<RecipeType[]> {\n // heic handling\n if (mimeType === 'image/heic') {\n this.log(title, 'šŸ”€');\n const jpeg = Buffer.from(\n await convert({\n // @ts-expect-error Not working with ArrayBuffer, but works with Buffer???\n buffer: data,\n format: 'JPEG',\n quality: 1,\n }),\n );\n return this.convertBuffer(title, jpeg, 'image/jpeg');\n }\n\n // pdf handling\n if (mimeType === 'application/pdf') {\n this.log(title, 'šŸ”€');\n const pages = await pdfToPng(data);\n this.log(title, true);\n\n const result: RecipeType[] = [];\n for (const page of pages) {\n result.push(\n ...(await this.convertBuffer(\n `${title} (p. ${page.pageNumber})`,\n page.content,\n 'image/png',\n )),\n );\n }\n\n return result;\n }\n\n this.log(title, 'šŸŖ„');\n const response = await this.client.chat.completions.create({\n messages: [\n {\n content: [\n {\n text: this.config.llm.prompt,\n type: 'text',\n },\n ],\n role: 'developer',\n },\n {\n content: [\n {\n image_url: {\n url: `data:${mimeType};base64,${data.toString('base64')}`,\n },\n type: 'image_url',\n },\n ],\n role: 'user',\n },\n ],\n model: this.config.llm.model,\n response_format: zodResponseFormat(Recipe, 'recipe'),\n });\n\n const choice = response.choices[0];\n if (!choice.message || !choice.message.content) {\n throw new Error(\n `Invalid response from LLM for file: ${title} (mime = ${mimeType}, length = ${data.length})`,\n );\n }\n\n let json;\n try {\n json = JSON.parse(choice.message.content);\n } catch (error) {\n throw new Error(\n `Failed to parse LLM response for file: ${title} (mime = ${mimeType}, length = ${data.length}): ${(error as Error).message}`,\n );\n }\n\n const recipe = Recipe.parse(json);\n this.log(title, true);\n return [recipe];\n }\n\n protected async convertFile(file: string): Promise<RecipeType[]> {\n const ext = file.slice(file.lastIndexOf('.')).toLowerCase();\n const mimeType = mimeTypes.get(ext);\n if (mimeType) {\n return this.convertBuffer(file, await readFile(file), mimeType);\n }\n\n throw new Error(`Unsupported file format for file: ${file}`);\n }\n\n protected async convertFiles(files: string[] = []) {\n if (!files.length) {\n throw new Error('No input files specified for conversion.');\n }\n\n // Check all files exist\n for (const file of files) {\n if (!existsSync(file)) {\n throw new Error(`Unable to find file: ${file}`);\n }\n }\n\n // Convert files one by one\n const recipes: RecipeType[] = [];\n for (const file of files) {\n recipes.push(...(await this.convertFile(file)));\n }\n\n return recipes;\n }\n\n protected log(file: string, emojiOrDone: string | true) {\n const emoji = emojiOrDone === true ? 'āœ…' : emojiOrDone;\n this.options.stdout?.write(\n `\\r${emoji} ${file}${emojiOrDone === true ? '\\n' : ''}`,\n );\n }\n}\n","#!/usr/bin/env node\n'use strict';\n\nimport express, { type Express } from 'express';\nimport { Server } from 'http';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nclass AppServer {\n private app: Express;\n private server: Server;\n\n constructor() {\n this.app = express();\n this.app.use(\n express.raw({\n limit: '50mb',\n type: ['image/jpeg', 'image/png', 'image/webp', 'image/heic'],\n }),\n );\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n }\n\n static run() {\n new AppServer();\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n let authToken: null | string = null;\n Config.use().then((config) => {\n authToken = config.server.authToken || null;\n });\n\n this.app.post('/convert', (req, res) => {\n const authTokenHeader = req.headers['authorization'] || '';\n if (authToken && authTokenHeader !== `Bearer ${authToken}`) {\n res.sendStatus(401);\n return;\n }\n\n const contentType = req.headers['content-type'] || '';\n Converter.convert(Buffer.from(req.body), contentType)\n .then((response: Buffer) => {\n res.setHeader('Content-Type', 'application/octet-stream');\n res.setHeader('Content-Length', response.byteLength);\n res.setHeader(\n 'Content-Disposition',\n 'attachment; filename=\"recipes.paprikarecipes\"',\n );\n res.send(response);\n })\n .catch((error) => {\n console.error(error);\n res.sendStatus(500);\n });\n });\n\n // add additional routes\n }\n\n async stop() {\n await new Promise((cb) => this.server.close(cb));\n\n // await db.close() if we have a db connection in this app\n // await other things we should cleanup nicely\n\n process.exit();\n }\n}\n\nAppServer.run();\n","#!/usr/bin/env node\n'use strict';\n\nimport { writeFile } from 'node:fs/promises';\nimport revealFile from 'reveal-file';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nyargs(hideBin(process.argv))\n .usage('$0 <cmd> [args]')\n .command(\n 'setup',\n 'Create or update the configuration file',\n () => {},\n () => Config.setup(),\n )\n .command(\n 'convert [files..]',\n 'Convert supported files to a .paprikarecipes file',\n (yargs) => {\n yargs.positional('files', {\n array: true,\n demandOption: true,\n describe: 'Input files to convert',\n type: 'string',\n });\n },\n async (argv) => {\n const files = argv.files as string[];\n const recipes = await Converter.convert(files, {\n stdout: process.stdout,\n });\n\n const outputFile = await Converter.generateOutputFilePath(files);\n await writeFile(outputFile, recipes);\n await revealFile(outputFile);\n },\n )\n .command(\n 'server',\n 'Launch the server',\n () => {},\n () => {\n import('./start.ts').catch((error) => {\n console.error('Failed to start the server:', error);\n process.exit(1);\n });\n },\n )\n .demandCommand(1, 1)\n .strict()\n .help('h')\n .parse();\n"],"mappings":"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","names":["import_node_crypto","import_node_fs","import_promises","import_node_path","import_prompts","Config","init_config","__esmMin","_Config","config","content","err","json","llm","prompts","value","output","error","toRecipes","recipes","zip","archiver","zipStream","chunks","chunk","recipe","data","toServings","resolve","reject","servings","number","import_archiver","import_node_stream","z","Recipe","init_recipe","__esmMin","import_heic_convert","import_node_fs","import_promises","import_node_path","import_openai","import_zod","import_pdf_to_png_converter","mimeTypes","Converter","init_convert","__esmMin","init_config","init_recipe","_Converter","config","options","OpenAI","arg1","arg2","arg3","Config","recipes","toRecipes","inputFiles","fileName","path","counter","title","data","mimeType","jpeg","convert","pages","result","page","choice","Recipe","json","error","recipe","file","ext","files","emojiOrDone","emoji","start_exports","import_express","AppServer","init_start","__esmMin","init_config","init_convert","_AppServer","express","req","res","authToken","Config","config","authTokenHeader","contentType","Converter","response","error","cb","import_promises","import_reveal_file","import_yargs","import_helpers","init_config","init_convert","yargs","Config","argv","files","recipes","Converter","outputFile","revealFile","error"]}
package/dist/bin/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{a as o,d as t}from"../chunk-T4PZ6TYU.js";import{writeFile as a}from"fs/promises";import n from"yargs";import{hideBin as p}from"yargs/helpers";n(p(process.argv)).usage("$0 <cmd> [args]").command("setup","Create or update the configuration file",()=>{},()=>o.setup()).command("convert [files..]","Convert supported files to a .paprikarecipes file",e=>{e.positional("files",{array:!0,demandOption:!0,describe:"Input files to convert",type:"string"})},async e=>{let r=e.files,s=await t.convert(r,{stdout:process.stdout}),i=await t.generateOutputFilePath(r);await a(i,s)}).command("server","Launch the server",()=>{},()=>{import("./start.js").catch(e=>{console.error("Failed to start the server:",e),process.exit(1)})}).demandCommand(1,1).strict().help("h").parse();
2
+ import{a as i,d as t}from"../chunk-T4PZ6TYU.js";import{writeFile as a}from"fs/promises";import n from"reveal-file";import p from"yargs";import{hideBin as c}from"yargs/helpers";p(c(process.argv)).usage("$0 <cmd> [args]").command("setup","Create or update the configuration file",()=>{},()=>i.setup()).command("convert [files..]","Convert supported files to a .paprikarecipes file",e=>{e.positional("files",{array:!0,demandOption:!0,describe:"Input files to convert",type:"string"})},async e=>{let r=e.files,s=await t.convert(r,{stdout:process.stdout}),o=await t.generateOutputFilePath(r);await a(o,s),await n(o)}).command("server","Launch the server",()=>{},()=>{import("./start.js").catch(e=>{console.error("Failed to start the server:",e),process.exit(1)})}).demandCommand(1,1).strict().help("h").parse();
3
3
  //# sourceMappingURL=cli.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport { writeFile } from 'node:fs/promises';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nyargs(hideBin(process.argv))\n .usage('$0 <cmd> [args]')\n .command(\n 'setup',\n 'Create or update the configuration file',\n () => {},\n () => Config.setup(),\n )\n .command(\n 'convert [files..]',\n 'Convert supported files to a .paprikarecipes file',\n (yargs) => {\n yargs.positional('files', {\n array: true,\n demandOption: true,\n describe: 'Input files to convert',\n type: 'string',\n });\n },\n async (argv) => {\n const files = argv.files as string[];\n const recipes = await Converter.convert(files, {\n stdout: process.stdout,\n });\n\n const outputFile = await Converter.generateOutputFilePath(files);\n await writeFile(outputFile, recipes);\n },\n )\n .command(\n 'server',\n 'Launch the server',\n () => {},\n () => {\n import('./start.ts').catch((error) => {\n console.error('Failed to start the server:', error);\n process.exit(1);\n });\n },\n )\n .demandCommand(1, 1)\n .strict()\n .help('h')\n .parse();\n"],"mappings":";gDAGA,OAAS,aAAAA,MAAiB,cAC1B,OAAOC,MAAW,QAClB,OAAS,WAAAC,MAAe,gBAKxBC,EAAMC,EAAQ,QAAQ,IAAI,CAAC,EACtB,MAAM,iBAAiB,EACvB,QACG,QACA,0CACA,IAAM,CAAC,EACP,IAAMC,EAAO,MAAM,CACvB,EACC,QACG,oBACA,oDACCF,GAAU,CACPA,EAAM,WAAW,QAAS,CACtB,MAAO,GACP,aAAc,GACd,SAAU,yBACV,KAAM,QACV,CAAC,CACL,EACA,MAAOG,GAAS,CACZ,IAAMC,EAAQD,EAAK,MACbE,EAAU,MAAMC,EAAU,QAAQF,EAAO,CAC3C,OAAQ,QAAQ,MACpB,CAAC,EAEKG,EAAa,MAAMD,EAAU,uBAAuBF,CAAK,EAC/D,MAAMI,EAAUD,EAAYF,CAAO,CACvC,CACJ,EACC,QACG,SACA,oBACA,IAAM,CAAC,EACP,IAAM,CACF,OAAO,YAAY,EAAE,MAAOI,GAAU,CAClC,QAAQ,MAAM,8BAA+BA,CAAK,EAClD,QAAQ,KAAK,CAAC,CAClB,CAAC,CACL,CACJ,EACC,cAAc,EAAG,CAAC,EAClB,OAAO,EACP,KAAK,GAAG,EACR,MAAM","names":["writeFile","yargs","hideBin","yargs","hideBin","Config","argv","files","recipes","Converter","outputFile","writeFile","error"]}
1
+ {"version":3,"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport { writeFile } from 'node:fs/promises';\nimport revealFile from 'reveal-file';\nimport yargs from 'yargs';\nimport { hideBin } from 'yargs/helpers';\n\nimport { Config } from '../lib/config.js';\nimport { Converter } from '../lib/convert.js';\n\nyargs(hideBin(process.argv))\n .usage('$0 <cmd> [args]')\n .command(\n 'setup',\n 'Create or update the configuration file',\n () => {},\n () => Config.setup(),\n )\n .command(\n 'convert [files..]',\n 'Convert supported files to a .paprikarecipes file',\n (yargs) => {\n yargs.positional('files', {\n array: true,\n demandOption: true,\n describe: 'Input files to convert',\n type: 'string',\n });\n },\n async (argv) => {\n const files = argv.files as string[];\n const recipes = await Converter.convert(files, {\n stdout: process.stdout,\n });\n\n const outputFile = await Converter.generateOutputFilePath(files);\n await writeFile(outputFile, recipes);\n await revealFile(outputFile);\n },\n )\n .command(\n 'server',\n 'Launch the server',\n () => {},\n () => {\n import('./start.ts').catch((error) => {\n console.error('Failed to start the server:', error);\n process.exit(1);\n });\n },\n )\n .demandCommand(1, 1)\n .strict()\n .help('h')\n .parse();\n"],"mappings":";gDAGA,OAAS,aAAAA,MAAiB,cAC1B,OAAOC,MAAgB,cACvB,OAAOC,MAAW,QAClB,OAAS,WAAAC,MAAe,gBAKxBC,EAAMC,EAAQ,QAAQ,IAAI,CAAC,EACtB,MAAM,iBAAiB,EACvB,QACG,QACA,0CACA,IAAM,CAAC,EACP,IAAMC,EAAO,MAAM,CACvB,EACC,QACG,oBACA,oDACCF,GAAU,CACPA,EAAM,WAAW,QAAS,CACtB,MAAO,GACP,aAAc,GACd,SAAU,yBACV,KAAM,QACV,CAAC,CACL,EACA,MAAOG,GAAS,CACZ,IAAMC,EAAQD,EAAK,MACbE,EAAU,MAAMC,EAAU,QAAQF,EAAO,CAC3C,OAAQ,QAAQ,MACpB,CAAC,EAEKG,EAAa,MAAMD,EAAU,uBAAuBF,CAAK,EAC/D,MAAMI,EAAUD,EAAYF,CAAO,EACnC,MAAMI,EAAWF,CAAU,CAC/B,CACJ,EACC,QACG,SACA,oBACA,IAAM,CAAC,EACP,IAAM,CACF,OAAO,YAAY,EAAE,MAAOG,GAAU,CAClC,QAAQ,MAAM,8BAA+BA,CAAK,EAClD,QAAQ,KAAK,CAAC,CAClB,CAAC,CACL,CACJ,EACC,cAAc,EAAG,CAAC,EAClB,OAAO,EACP,KAAK,GAAG,EACR,MAAM","names":["writeFile","revealFile","yargs","hideBin","yargs","hideBin","Config","argv","files","recipes","Converter","outputFile","writeFile","revealFile","error"]}
package/package.json CHANGED
@@ -13,6 +13,7 @@
13
13
  "openai": "^6.8.1",
14
14
  "pdf-to-png-converter": "^3.10.0",
15
15
  "prompts": "^2.4.2",
16
+ "reveal-file": "^0.1.0",
16
17
  "yargs": "^18.0.0",
17
18
  "zod": "^4.1.12"
18
19
  },
@@ -80,5 +81,5 @@
80
81
  "test": "mocha"
81
82
  },
82
83
  "type": "module",
83
- "version": "1.0.1-develop.2"
84
+ "version": "1.1.0-develop.1"
84
85
  }