This commit is contained in:
lovebird 2025-04-06 16:03:02 +02:00
parent a29bae4136
commit 4a08425c24
9 changed files with 465 additions and 67 deletions

View File

@ -23,7 +23,7 @@
"format": "unix-time"
}
],
"default": "2025-04-06T09:23:22.935Z"
"default": "2025-04-06T13:49:04.457Z"
},
"description": {
"type": "string",

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
"messages": [
{
"role": "user",
"content": "Return a list of useful references, as Markdown, grouped : Articles, Books, Papers, Youtube, Software, Opensource Designs, ... Dont comment!\n\nText to process:\nThis tutorial demonstrates the process of cutting HDPE sheets with an X-Carve CNC. You can watch the complete video in Spanish with subtitles [here](https://www.youtube.com/watch?v=4LrrFz802To).\n\n\nUser Location: Mexico City, Mexico\n\nFor this step, measure the plastic sheet's height, width, and thickness. Our X-Carve machine uses the CAM software Easel for CNC milling. Easel allows you to simulate your material, and it includes HDPE 2-Colors in the cutting material list.\n\nUsing CNC clamps from the X-Carve, secure the sheet to the table.\n\nProceed to a vector graphics editor, like Inkscape, to create a design, or download an open-source vector file from [The Noun Project](https://thenounproject.com).\n\nDownload the SVG file and import it into Easel.\n\n### Instructions for CNC Router Setup\n\n1. Select the desired cutting width in the file.\n2. Ensure the sheet is securely fixed.\n3. Choose the cutting bit: 1/8 inch (3.175 mm) flat flute.\n4. Set the machine's origin to the bottom left corner.\n5. Raise the bit and activate the CNC router.\n\nTake your glasses or object, complete the post-processing, and share the results with others.\n\nYou can attempt this project using various CNC machines, including manual routers or saws, as demonstrated in this video: [youtu.be](https://youtu.be/gxkcffQD3eQ). It is important to share your work to support community growth.\n\nContributions and feedback are welcome."
"content": "Return a list of useful references, as Markdown, grouped : Articles, Books, Papers, Youtube, Software, Opensource Designs, ... Dont comment!\n\nText to process:\nThis tutorial explains the process of cutting HDPE sheets using an X-Carve CNC. \n\nWatch the full video in Spanish with subtitles [here](https://www.youtube.com/watch?v=4LrrFz802To).\n\n\nUser Location: Mexico City, Mexico\n\n### Step Instructions:\n\nMeasure the plastic sheet's height, width, and thickness. The X-Carve machine uses the CAM software Easel, which facilitates CNC milling. Easel allows you to simulate your material, and it includes HDPE two-color options in its material list.\n\n- **Metric to Imperial Conversion:** \n Height, Width, Thickness: Convert as needed, e.g., 1 cm = 0.3937 in.\n\nI'm sorry, but I'm unable to process the task given the provided parameters. Could you please provide more information or clarify your request?\n\nProceed to a vector graphics editor like Inkscape to create a vector file or download one from [The Noun Project](https://thenounproject.com).\n\nDownload the SVG file, a vector format, and import it to Easel.\n\nWith the file ready, select the desired width for carving or cutting, and proceed to initiate the cut using the wizard:\n\n- Confirm that the sheet is secure.\n- Specify the cutting bit; a 1/8 inch (3.175 mm) flat flute bit is used.\n- Set the machine's 0-0 coordinate, always selecting the lower left corner.\n- Raise the bit and activate the CNC Router.\n\nTake your glasses or object, post-process them, and then share with others.\n\nThis project can also be attempted with various CNC machines or manual tools like routers and saws, as demonstrated [here](https://youtu.be/gxkcffQD3eQ). Sharing your work and contributing to the community is encouraged.\n\nFeel free to share your ideas and comments."
},
{
"role": "user",

119
src/base/async-iterator.ts Normal file
View File

@ -0,0 +1,119 @@
import { JSONPath } from 'jsonpath-plus'
import pThrottle from 'p-throttle'
import pMap from 'p-map'
export type AsyncTransformer = (input: string, path: string) => Promise<string>
export type ErrorCallback = (path: string, value: string, error: any) => void
export type FilterCallback = (input: string, path: string) => Promise<boolean>
export type Filter = (input: string) => Promise<boolean>
export interface TransformOptions {
transform: AsyncTransformer
path: string
throttleDelay: number
concurrentTasks: number
errorCallback: ErrorCallback
filterCallback: FilterCallback
}
export const isNumber: Filter = async (input: string) => (/^-?\d+(\.\d+)?$/.test(input))
export const isBoolean: Filter = async (input: string) => /^(true|false)$/i.test(input)
export const isValidString: Filter = async (input: string) => !(input.trim() !== '')
export const testFilters = (filters: Filter[]): FilterCallback => {
return async (input: string) => {
for (const filter of filters) {
if (await filter(input)) {
return false;
}
}
return true;
};
};
export const defaultFilters = (filters: Filter[] = []) =>
[
isNumber, isBoolean, isValidString, ...filters
]
export async function transformObject(
obj: any,
transform: AsyncTransformer,
path: string,
throttleDelay: number,
concurrentTasks: number,
errorCallback: ErrorCallback,
testCallback: FilterCallback
): Promise<void> {
const paths = JSONPath({ path, json: obj, resultType: 'pointer' });
await pMap(
paths,
async (jsonPointer: any) => {
const keys = jsonPointer.slice(1).split('/')
await transformPath(obj, keys, transform, throttleDelay, concurrentTasks, jsonPointer, errorCallback, testCallback)
},
{ concurrency: concurrentTasks }
)
}
export async function transformPath(
obj: any,
keys: string[],
transform: AsyncTransformer,
throttleDelay: number,
concurrentTasks: number,
currentPath: string,
errorCallback: ErrorCallback,
testCallback: FilterCallback
): Promise<void> {
let current = obj
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]]
}
const lastKey = keys[keys.length - 1]
const throttle = pThrottle({
limit: 1,
interval: throttleDelay,
})
if (typeof lastKey === 'string' && lastKey !== '') {
try {
const newKey = isValidString(lastKey) && !isNumber(lastKey) ? await throttle(transform)(lastKey, currentPath) : lastKey
if (newKey !== lastKey) {
current[newKey] = current[lastKey]
delete current[lastKey]
}
if (typeof current[newKey] === 'string' && current[newKey] !== '') {
if (await testCallback(current[newKey], `${currentPath}/${lastKey}`)) {
current[newKey] = await throttle(transform)(current[newKey], `${currentPath}/${lastKey}`)
}
} else if (typeof current[newKey] === 'object' && current[newKey] !== null) {
await transformObject(current[newKey], transform, '$.*', throttleDelay, concurrentTasks, errorCallback, testCallback)
}
} catch (error) {
errorCallback(currentPath, lastKey, error)
}
}
}
const exampleTransformFunction: AsyncTransformer = async (input: string, path: string): Promise<string> => {
if (input === 'random') throw new Error('API error')
return input.toUpperCase()
}
export const defaultError: ErrorCallback = (path: string, value: string, error: any): void => {
// logger.error(`Error at path: ${path}, value: ${value}, error: ${error}`)
}
export const defaultOptions = (options: TransformOptions = {} as TransformOptions): TransformOptions => {
return {
transform: exampleTransformFunction,
path: options.path || '$[*][0,1,2]',
throttleDelay: 10,
concurrentTasks: 1,
errorCallback: defaultError,
filterCallback: testFilters(defaultFilters()),
...options
}
}

View File

@ -6,37 +6,12 @@ import { meta } from '../base/url.js';
export interface FilterFunction { (text: string): string | Promise<string> }
export const BLACKLIST: readonly string[] = [
'precious-plastic',
'fair-enough',
'mad-plastic-labs',
'easymoulds',
'plasticpreneur',
'sustainable-design-studio',
'johannplasto'
] as const;
import filterConfig from "config/filters.json" assert { type: "json" };
export const URL_BLACKLIST: readonly string[] = [
"preciousplastic.com",
"community.preciousplastic.com",
"bazar.preciousplastic.com",
"onearmy.earth",
"davehakkens.nl",
"sustainabledesign.studio"
] as const;
export const WORD_BLACKLIST: readonly string[] = [
"wizard",
"magic2",
"precious plastic",
"onearmy"
] as const;
export const FILTER_MAP: Readonly<Record<string, string>> = {
Router: "CNC Router",
"laptop stand": "laptoppie",
Car: "tufftuff"
} as const;
export const BLACKLIST = filterConfig.BLACKLIST;
export const URL_BLACKLIST = filterConfig.URL_BLACKLIST;
export const WORD_BLACKLIST = filterConfig.WORD_BLACKLIST;
export const FILTER_MAP: Record<string, string> = filterConfig.FILTER_MAP;
/**
* Shortens a URL by removing 'www.' prefix and trailing slashes

View File

@ -0,0 +1,267 @@
---
import fs from "fs";
import path from "path";
import { decode } from "html-entities";
import { IHowto, asset_local_rel } from "@/model/howto/howto.js";
import { Img } from "imagetools/components";
import { i18n as Translate } from "@polymech/astro-base";
import BaseLayout from "@/layouts/BaseLayout.astro";
import Wrapper from "@/components/containers/Wrapper.astro";
import GalleryK from "@/components/polymech/gallery.astro";
import { files, forward_slash } from "@polymech/commons";
import pMap from "p-map";
import { sync as exists } from "@polymech/fs/exists";
import { sync as read } from "@polymech/fs/read";
import { createHTMLComponent, createMarkdownComponent } from "@/base/index.js";
import { translate } from "@/base/i18n.js";
import { applyFilters, shortenUrl } from "@/base/filters.js";
// import { extract, extract_learned_skills, references } from "@/base/kbot.js";
import {
HOWTO_FILES_WEB,
HOWTO_FILES_ABS,
I18N_SOURCE_LANGUAGE,
HOWTO_COMPLETE_RESOURCES,
HOWTO_COMPLETE_SKILLS,
HOWTO_ADD_HARDWARE,
HOWTO_LOCAL_RESOURCES,
HOWTO_ADD_RESOURCES,
HOWTO_ADD_REFERENCES,
HOWTO_EDIT_URL,
} from "config/config.js";
import { filter } from "@/base/kbot.js";
interface Props {
howto: IHowto;
}
const { frontmatter: howto } = Astro.props;
const howto_abs = HOWTO_FILES_ABS(howto.slug);
let model_files: any = [...files(howto_abs, "**/**/*.(step|stp)")];
model_files = model_files.map((f) =>
forward_slash(`${howto.slug}/${path.relative(path.resolve(howto_abs), f)}`),
);
const content = async (str: string) =>
await translate(str, I18N_SOURCE_LANGUAGE, Astro.currentLocale);
const component = async (str: string) =>
await createMarkdownComponent(await content(str));
const componentHTML = async (str: string) =>
await createHTMLComponent(await content(str));
const stepsWithFilteredMarkdown = await pMap(
howto.steps,
async (step) => ({
...step,
filteredMarkdownComponent: await component(step.text),
}),
{ concurrency: 1 },
);
const Description = component(howto.description);
const authorGeo = howto?.user?.geo || {
countryName: "Unknown",
data: { urls: [] },
};
const authorLinks = (howto?.user?.data.urls || []).filter(
(l) => !l.url.includes("one_army") && !l.url.includes("bazar"),
);
//////////////////////////////////////////////////////////////
//
// Resources
//
//////////////////////////////////////////////////////////////
const howto_resources_default = `# Resources`;
const howto_resources_path = path.join(howto_abs, "resources.md");
let howto_resources = exists(howto_resources_path)
? read(howto_resources_path, "string") || howto_resources_default
: howto_resources_default;
const howto_references_default = `# References`;
const howto_references_path = path.join(howto_abs, "references.md");
let howto_references = exists(howto_resources_path)
? read(howto_references_path, "string") || howto_references_default
: howto_references_default;
const contentAll = `${howto.content}`;
if (HOWTO_COMPLETE_SKILLS) {
const references_extra = await filter(contentAll, "learned_skills");
howto_resources = `${howto_resources}\n\n ${references_extra}`;
}
if (HOWTO_LOCAL_RESOURCES && howto.user && howto.user.geo) {
const references_extra = await filter(
`Location: ${authorGeo.countryName}`,
"local",
);
howto_resources = `${howto_resources}\n\n ${references_extra}`;
}
const Resources = component(howto_resources);
const References = component(howto_references);
/*
const EditLink = () => {
return (
<a href={HOWTO_EDIT_URL(howto.slug, Astro.currentLocale)}>Edit</a>
)
}
*/
---
<BaseLayout class="markdown-content bg-gray-100" frontmatter={howto}>
<Wrapper>
<article class="bg-white shadow-lg rounded-lg overflow-hidden">
<header class="p-4 ">
<h1 class="text-4xl font-bold text-gray-800 mb-4">
<Translate>{howto.title}</Translate>
</h1>
<GalleryK images={[{ src: howto.cover_image.src, alt: "" }]} />
<div class="flex flex-wrap gap-2 mb-4">
{
howto.tags.map((tag) => (
<span class="bg-orange-400 text-white text-xs px-3 py-1 rounded-full">
<Translate>{tag.toUpperCase()}</Translate>
</span>
))
}
</div>
</header>
</article>
<section class="meta-view bg-white rounded-lg p-4 mt-4 truncate">
<ul class="grid md:grid-cols-1 lg:grid-cols-2 gap-4 mt-8 mb-8">
<li>
<strong><Translate>Difficulty:</Translate></strong>
<Translate>{howto.difficulty_level}</Translate>
</li>
<li>
<strong><Translate>Time Required:</Translate></strong>
<Translate>{decode(howto.time)}</Translate>
</li>
<li>
<strong><Translate>Views:</Translate></strong>{howto.total_views}
</li>
<li>
<strong><Translate>Creator:</Translate></strong>{howto._createdBy}
</li>
<li>
<strong><Translate>Country:</Translate></strong>{authorGeo.countryName }
</li>
<li>
<strong><Translate>Email:</Translate></strong>
<a
class="text-orange-600 underline"
href={`mailto:${authorLinks.find((link) => link.name.toLowerCase() === "email")?.url.replace("mailto:", "")}`}
>
{
authorLinks
.find((link) => link.name.toLowerCase() === "email")
?.url.replace("mailto:", "")
}
</a>
</li>
{
authorLinks
.filter((l) => l.name.toLowerCase() !== "email")
.map((link) => (
<li>
<strong>{link.name}:</strong>
<a
class="text-orange-600 underline"
href={link.url}
target="_blank"
>
{shortenUrl(link.url)}
</a>
</li>
))
}
<li>
<strong><Translate>Downloads:</Translate></strong>{
howto.total_downloads
}
</li>
</ul>
</section>
<section class="bg-white p-8">
<div class="mb-8 markdown-content">
<Description />
</div>
<a
href={HOWTO_FILES_WEB(howto.slug)}
class="inline-block py-2 px-4 bg-orange-500 hover:bg-orange-700 text-white rounded-full mb-8"
><Translate>Browse Files</Translate></a
>
</section>
<section class="px-8 py-8 bg-orange-50">
<h2 class="font-bold mb-4 text-xl">
<Translate>Table of Contents</Translate>
</h2>
<ul class="grid grid-cols-1 md:grid-cols-2 gap-2 list-decimal p-4">
{
stepsWithFilteredMarkdown.map((step, idx) => (
<li>
<a
href={`#step-${idx + 1}`}
class="text-orange-600 hover:underline"
>
<Translate>{step.title}</Translate>
</a>
</li>
))
}
</ul>
</section>
<section class="border-gray-300 p-0 lg:p-6 mt-8">
<ol class="space-y-10">
{
stepsWithFilteredMarkdown.map((step, idx) => (
<li
id={`step-${idx + 1}`}
class="bg-white shadow-sm rounded-lg p-2 lg:p-6"
>
<div class="mb-4 flex items-center">
<span class="bg-orange-500 text-xl font-bold text-white rounded-full h-10 w-10 flex items-center justify-center mr-3">
{idx + 1}
</span>
<h3 class="text-xl font-bold">
<a
href={`#step-${idx + 1}`}
class="text-orange-600 hover:underline"
>
<Translate>{step.title}</Translate>
</a>
</h3>
</div>
<div class="markdown-content">
<step.filteredMarkdownComponent />
</div>
{step.images?.length > 0 && <GalleryK images={step.images} />}
</li>
))
}
</ol>
</section>
<section class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"><Resources /></section>
<section class="bg-white shadow-lg rounded-lg border-gray-300 p-4 lg:p-6 mt-8 markdown-content"><References /></section>
<footer class="p-8 text-sm border-t bg-white text-gray-600">
<div class="flex justify-between">
<span
><Translate>Created on</Translate>: {
new Date(howto._created).toLocaleDateString()
}</span
>
<span
>{howto.votedUsefulBy.length}
<Translate>people found this useful</Translate></span
>
</div>
</footer>
</Wrapper>
</BaseLayout>

View File

@ -5,7 +5,7 @@ import { sync as read } from '@polymech/fs/read'
import { sanitizeUri } from 'micromark-util-sanitize-uri'
// LLM
export const LLM_CACHE = false
export const LLM_CACHE = true
export const LLM_CACHE_DIR = './.cache/kbot'
export const OSR_ROOT = () => path.resolve(resolve("${OSR_ROOT}"))
@ -32,11 +32,11 @@ export const HOWTO_ANNOTATIONS = false
export const HOWTO_ANNOTATIONS_CACHE = false
export const HOWTO_COMPLETE_RESOURCES = true
export const HOWTO_ADD_HARDWARE = false
export const HOWTO_ADD_RESOURCES = true
export const HOWTO_ADD_REFERENCES = true
export const HOWTO_ADD_RESOURCES = false
export const HOWTO_ADD_REFERENCES = false
export const HOWTO_COMPLETE_SKILLS = false
export const HOWTO_LOCAL_RESOURCES = false
export const HOWTO_SEO_LLM = true
export const HOWTO_SEO_LLM = false
export const HOWTO_MAX_ITEMS = 1
export const HOWTO_MIGRATION = () => path.resolve(resolve("./data/last.json"))

30
src/config/filters.json Normal file
View File

@ -0,0 +1,30 @@
{
"BLACKLIST": [
"precious-plastic",
"fair-enough",
"mad-plastic-labs",
"easymoulds",
"plasticpreneur",
"sustainable-design-studio",
"johannplasto"
],
"URL_BLACKLIST": [
"preciousplastic.com",
"community.preciousplastic.com",
"bazar.preciousplastic.com",
"onearmy.earth",
"davehakkens.nl",
"sustainabledesign.studio"
],
"WORD_BLACKLIST": [
"wizard",
"magic2",
"precious plastic",
"onearmy"
],
"FILTER_MAP": {
"Router": "CNC Router",
"laptop stand": "laptoppie",
"Car": "tufftuff"
}
}

View File

@ -168,10 +168,25 @@ export const defaults = async (data: any, cwd: string, root: string) => {
//
/////////////////////////////////////////////////////////////////////////
const commons = async (text: string): Promise<string> => {
return await template_filter(text, 'simple', TemplateContext.COMMONS);
// language filter (context: commons, options: simple), see src/config/templates/commons.json
const llm_language = async (text: string): Promise<string> => await template_filter(text, 'simple', TemplateContext.COMMONS)
// default filters (plain), see src/config/filters.json
const filters_default = async (str: string, filters: FilterFunction[] = default_filters_plain) => await applyFilters(str, filters)
// combined content filter that applies both filters in reverse order (LLM first, then filters)
const content = async (str: string, filters: FilterFunction[] = default_filters_plain): Promise<string> => {
// first apply LLM language filter
const llm_filtered = await template_filter(str, 'simple', TemplateContext.COMMONS)
// then apply default text filters
return await applyFilters(llm_filtered, [...filters, validateLinks])
}
const content = async (str: string, filters: FilterFunction[] = default_filters_plain) => await applyFilters(str, filters)
/////////////////////////////////////////////////////////////////////////
//
// Exports
//
/////////////////////////////////////////////////////////////////////////
const to_github = async (item: IHowto) => {
const itemDir = item_path(item)
@ -312,33 +327,27 @@ const complete = async (item: IHowto) => {
const configPath = path.join(item_path(item), 'config.json')
const config = read(configPath, 'json') as IHowto || {}
// item = { ...item, ...config }
if (!HOWTO_ANNOTATIONS) {
// return item
}
// commons: language, tone, bullshit filter, and a piece of love, just a bit, at least :)
if (HOWTO_FILTER_LLM) {
item.description = await commons(item.description || '')
}
item.description = await applyFilters(item.description || '', [validateLinks])
// default pass, links, words, formatting, ...
item.steps = await pMap(
item.steps,
async (step) => ({
...step,
text: await content(step.text)
})
)
// commons: language, tone, bullshit filter, and a piece of love, just a bit, at least :)
// Apply content filtering respecting HOWTO_FILTER_LLM flag
if (HOWTO_FILTER_LLM) {
item.description = await content(item.description || '')
// Process steps with content filtering
item.steps = await pMap(
item.steps,
async (step) => ({
...step,
text: await commons(step.text)
text: await content(step.text)
})
)
} else {
// Just apply validateLinks if LLM filtering is disabled
item.description = await applyFilters(item.description || '', [validateLinks])
// Apply only default filters to steps
item.steps = await pMap(
item.steps,
async (step) => ({
...step,
text: await filters_default(step.text)
})
)
}
@ -350,16 +359,14 @@ const complete = async (item: IHowto) => {
...item.steps.map(step => step.text)
].filter(Boolean).join('\n\n')
// Generate keywords using the keywords template
item.keywords = await template_filter(item.content, 'keywords', TemplateContext.HOWTO);
if (HOWTO_ADD_RESOURCES) {
item.keywords = await template_filter(item.content, 'keywords', TemplateContext.HOWTO);
item.resources = await template_filter(item.content, 'toolsAndHardware', TemplateContext.HOWTO);
// item.resources = await template_filter(item.content, 'toolsAndHardware', TemplateContext.HOWTO);
item.resources = await applyFilters(item.resources, default_filters_markdown);
write(path.join(item_path(item), 'resources.md'), item.resources as string)
}
if (HOWTO_ADD_REFERENCES) {
item.keywords = await template_filter(item.content, 'keywords', TemplateContext.HOWTO);
item.references = await template_filter(item.content, 'references', TemplateContext.HOWTO);
// item.references = await template_filter(item.content, 'references', TemplateContext.HOWTO);
item.references = await applyFilters(item.references, default_filters_markdown);
write(path.join(item_path(item), 'references.md'), item.references as string)
}